mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-09-07 00:36:36 +08:00
feat(config): 添加模型助手功能以选择和管理模型
本次提交主要包含以下更改: 1. **后端更新**: - 在 `app/service/config/config_service.py` 中新增 `fetch_ui_models` 方法,用于获取可用于 UI 的模型列表,并处理相关的错误情况。 - 在 `app/router/config_routes.py` 中新增 `/ui/models` 路由,提供模型列表的 API 接口,并添加身份验证逻辑。 2. **前端更新**: - 在 `app/static/js/config_editor.js` 中实现模型助手的功能,包括模型列表的加载、搜索和选择。 - 在 `app/templates/config_editor.html` 中添加模型助手的模态框和相关的 UI 元素,允许用户从列表中选择模型。 这些更改旨在增强用户体验,使用户能够更方便地选择和管理模型,提高配置界面的交互性和功能性。
This commit is contained in:
@@ -1,18 +1,21 @@
|
|||||||
"""
|
"""
|
||||||
配置路由模块
|
配置路由模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Request
|
from fastapi import APIRouter, HTTPException, Request
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
|
|
||||||
from app.core.security import verify_auth_token
|
from app.core.security import verify_auth_token
|
||||||
from app.log.logger import get_config_routes_logger, Logger
|
from app.log.logger import Logger, get_config_routes_logger
|
||||||
from app.service.config.config_service import ConfigService
|
from app.service.config.config_service import ConfigService
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/config", tags=["config"])
|
router = APIRouter(prefix="/api/config", tags=["config"])
|
||||||
|
|
||||||
logger = get_config_routes_logger()
|
logger = get_config_routes_logger()
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=Dict[str, Any])
|
@router.get("", response_model=Dict[str, Any])
|
||||||
async def get_config(request: Request):
|
async def get_config(request: Request):
|
||||||
auth_token = request.cookies.get("auth_token")
|
auth_token = request.cookies.get("auth_token")
|
||||||
@@ -49,3 +52,23 @@ async def reset_config(request: Request):
|
|||||||
return await ConfigService.reset_config()
|
return await ConfigService.reset_config()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/ui/models")
|
||||||
|
async def get_ui_models(request: Request):
|
||||||
|
auth_token_cookie = request.cookies.get("auth_token")
|
||||||
|
if not auth_token_cookie or not verify_auth_token(auth_token_cookie):
|
||||||
|
logger.warning("Unauthorized access attempt to /api/config/ui/models")
|
||||||
|
raise HTTPException(status_code=403, detail="Not authenticated")
|
||||||
|
|
||||||
|
try:
|
||||||
|
models = await ConfigService.fetch_ui_models()
|
||||||
|
return models
|
||||||
|
except HTTPException as e:
|
||||||
|
raise e
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error in /ui/models endpoint: {e}", exc_info=True)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"An unexpected error occurred while fetching UI models: {str(e)}",
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,41 +1,49 @@
|
|||||||
"""
|
"""
|
||||||
配置服务模块
|
配置服务模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from dotenv import find_dotenv, load_dotenv
|
from dotenv import find_dotenv, load_dotenv
|
||||||
|
from fastapi import HTTPException
|
||||||
from sqlalchemy import insert, update
|
from sqlalchemy import insert, update
|
||||||
|
|
||||||
|
from app.config.config import Settings as ConfigSettings
|
||||||
from app.config.config import settings
|
from app.config.config import settings
|
||||||
from app.database.connection import database
|
from app.database.connection import database
|
||||||
from app.database.models import Settings
|
from app.database.models import Settings
|
||||||
from app.config.config import Settings as ConfigSettings
|
|
||||||
from app.database.services import get_all_settings
|
from app.database.services import get_all_settings
|
||||||
from app.service.key.key_manager import get_key_manager_instance, reset_key_manager_instance
|
|
||||||
from app.log.logger import get_config_routes_logger
|
from app.log.logger import get_config_routes_logger
|
||||||
|
from app.service.key.key_manager import (
|
||||||
|
get_key_manager_instance,
|
||||||
|
reset_key_manager_instance,
|
||||||
|
)
|
||||||
|
from app.service.model.model_service import ModelService
|
||||||
|
|
||||||
logger = get_config_routes_logger()
|
logger = get_config_routes_logger()
|
||||||
|
|
||||||
|
|
||||||
class ConfigService:
|
class ConfigService:
|
||||||
"""配置服务类,用于管理应用程序配置"""
|
"""配置服务类,用于管理应用程序配置"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_config() -> Dict[str, Any]:
|
async def get_config() -> Dict[str, Any]:
|
||||||
return settings.model_dump()
|
return settings.model_dump()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update_config(config_data: Dict[str, Any]) -> Dict[str, Any]:
|
async def update_config(config_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
for key, value in config_data.items():
|
for key, value in config_data.items():
|
||||||
if hasattr(settings, key):
|
if hasattr(settings, key):
|
||||||
setattr(settings, key, value)
|
setattr(settings, key, value)
|
||||||
logger.debug(f"Updated setting in memory: {key}")
|
logger.debug(f"Updated setting in memory: {key}")
|
||||||
|
|
||||||
# 获取现有设置
|
# 获取现有设置
|
||||||
existing_settings_raw: List[Dict[str, Any]] = await get_all_settings()
|
existing_settings_raw: List[Dict[str, Any]] = await get_all_settings()
|
||||||
existing_settings_map: Dict[str, Dict[str, Any]] = {s['key']: s for s in existing_settings_raw}
|
existing_settings_map: Dict[str, Dict[str, Any]] = {
|
||||||
|
s["key"]: s for s in existing_settings_raw
|
||||||
|
}
|
||||||
existing_keys = set(existing_settings_map.keys())
|
existing_keys = set(existing_settings_map.keys())
|
||||||
|
|
||||||
settings_to_update: List[Dict[str, Any]] = []
|
settings_to_update: List[Dict[str, Any]] = []
|
||||||
@@ -47,7 +55,7 @@ class ConfigService:
|
|||||||
# 处理不同类型的值
|
# 处理不同类型的值
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
db_value = json.dumps(value)
|
db_value = json.dumps(value)
|
||||||
elif isinstance(value, dict): # 新增对 dict 类型的处理
|
elif isinstance(value, dict): # 新增对 dict 类型的处理
|
||||||
db_value = json.dumps(value)
|
db_value = json.dumps(value)
|
||||||
elif isinstance(value, bool):
|
elif isinstance(value, bool):
|
||||||
db_value = str(value).lower()
|
db_value = str(value).lower()
|
||||||
@@ -55,24 +63,26 @@ class ConfigService:
|
|||||||
db_value = str(value)
|
db_value = str(value)
|
||||||
|
|
||||||
# 仅当值发生变化时才更新
|
# 仅当值发生变化时才更新
|
||||||
if key in existing_keys and existing_settings_map[key]['value'] == db_value:
|
if key in existing_keys and existing_settings_map[key]["value"] == db_value:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
description = f"{key}配置项"
|
description = f"{key}配置项"
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'key': key,
|
"key": key,
|
||||||
'value': db_value,
|
"value": db_value,
|
||||||
'description': description,
|
"description": description,
|
||||||
'updated_at': now
|
"updated_at": now,
|
||||||
}
|
}
|
||||||
|
|
||||||
if key in existing_keys:
|
if key in existing_keys:
|
||||||
# Preserve original description if not explicitly provided
|
# Preserve original description if not explicitly provided
|
||||||
data['description'] = existing_settings_map[key].get('description', description)
|
data["description"] = existing_settings_map[key].get(
|
||||||
|
"description", description
|
||||||
|
)
|
||||||
settings_to_update.append(data)
|
settings_to_update.append(data)
|
||||||
else:
|
else:
|
||||||
data['created_at'] = now
|
data["created_at"] = now
|
||||||
settings_to_insert.append(data)
|
settings_to_insert.append(data)
|
||||||
|
|
||||||
# 在事务中执行批量插入和更新
|
# 在事务中执行批量插入和更新
|
||||||
@@ -82,17 +92,19 @@ class ConfigService:
|
|||||||
if settings_to_insert:
|
if settings_to_insert:
|
||||||
query_insert = insert(Settings).values(settings_to_insert)
|
query_insert = insert(Settings).values(settings_to_insert)
|
||||||
await database.execute(query=query_insert)
|
await database.execute(query=query_insert)
|
||||||
logger.info(f"Bulk inserted {len(settings_to_insert)} settings.")
|
logger.info(
|
||||||
|
f"Bulk inserted {len(settings_to_insert)} settings."
|
||||||
|
)
|
||||||
|
|
||||||
if settings_to_update:
|
if settings_to_update:
|
||||||
for setting_data in settings_to_update:
|
for setting_data in settings_to_update:
|
||||||
query_update = (
|
query_update = (
|
||||||
update(Settings)
|
update(Settings)
|
||||||
.where(Settings.key == setting_data['key'])
|
.where(Settings.key == setting_data["key"])
|
||||||
.values(
|
.values(
|
||||||
value=setting_data['value'],
|
value=setting_data["value"],
|
||||||
description=setting_data['description'],
|
description=setting_data["description"],
|
||||||
updated_at=setting_data['updated_at']
|
updated_at=setting_data["updated_at"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
await database.execute(query=query_update)
|
await database.execute(query=query_update)
|
||||||
@@ -112,7 +124,7 @@ class ConfigService:
|
|||||||
# For now, we log the error and continue
|
# For now, we log the error and continue
|
||||||
|
|
||||||
return await ConfigService.get_config()
|
return await ConfigService.get_config()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def reset_config() -> Dict[str, Any]:
|
async def reset_config() -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
@@ -124,7 +136,9 @@ class ConfigService:
|
|||||||
"""
|
"""
|
||||||
# 1. 重新加载配置对象,它应该处理环境变量和 .env 的优先级
|
# 1. 重新加载配置对象,它应该处理环境变量和 .env 的优先级
|
||||||
_reload_settings()
|
_reload_settings()
|
||||||
logger.info("Settings object reloaded, prioritizing system environment variables then .env file.")
|
logger.info(
|
||||||
|
"Settings object reloaded, prioritizing system environment variables then .env file."
|
||||||
|
)
|
||||||
|
|
||||||
# 2. 重置并重新初始化 KeyManager
|
# 2. 重置并重新初始化 KeyManager
|
||||||
try:
|
try:
|
||||||
@@ -140,6 +154,36 @@ class ConfigService:
|
|||||||
# 3. 返回更新后的配置
|
# 3. 返回更新后的配置
|
||||||
return await ConfigService.get_config()
|
return await ConfigService.get_config()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def fetch_ui_models() -> List[Dict[str, Any]]:
|
||||||
|
"""获取用于UI显示的模型列表"""
|
||||||
|
try:
|
||||||
|
key_manager = await get_key_manager_instance()
|
||||||
|
model_service = ModelService()
|
||||||
|
|
||||||
|
api_key = await key_manager.get_first_valid_key()
|
||||||
|
if not api_key:
|
||||||
|
logger.error("No valid API keys available to fetch model list for UI.")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="No valid API keys available to fetch model list.",
|
||||||
|
)
|
||||||
|
|
||||||
|
models = await model_service.get_gemini_openai_models(api_key)
|
||||||
|
return models
|
||||||
|
except HTTPException as e:
|
||||||
|
# Re-raise HTTPExceptions directly if they are already specific
|
||||||
|
raise e
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Failed to fetch models for UI in ConfigService: {e}", exc_info=True
|
||||||
|
)
|
||||||
|
# Raise a generic HTTPException for other errors
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500, detail=f"Failed to fetch models for UI: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# 重新加载配置的函数
|
# 重新加载配置的函数
|
||||||
def _reload_settings():
|
def _reload_settings():
|
||||||
"""重新加载环境变量并更新配置"""
|
"""重新加载环境变量并更新配置"""
|
||||||
@@ -147,4 +191,4 @@ def _reload_settings():
|
|||||||
load_dotenv(find_dotenv(), override=True)
|
load_dotenv(find_dotenv(), override=True)
|
||||||
# 更新现有 settings 对象的属性,而不是新建实例
|
# 更新现有 settings 对象的属性,而不是新建实例
|
||||||
for key, value in ConfigSettings().model_dump().items():
|
for key, value in ConfigSettings().model_dump().items():
|
||||||
setattr(settings, key, value)
|
setattr(settings, key, value)
|
||||||
|
|||||||
@@ -31,6 +31,23 @@ const bulkDeleteProxyInput = document.getElementById("bulkDeleteProxyInput");
|
|||||||
const resetConfirmModal = document.getElementById("resetConfirmModal");
|
const resetConfirmModal = document.getElementById("resetConfirmModal");
|
||||||
const configForm = document.getElementById("configForm"); // Added for frequent use
|
const configForm = document.getElementById("configForm"); // Added for frequent use
|
||||||
|
|
||||||
|
// Model Helper Modal Elements
|
||||||
|
const modelHelperModal = document.getElementById("modelHelperModal");
|
||||||
|
const modelHelperTitleElement = document.getElementById("modelHelperTitle");
|
||||||
|
const modelHelperSearchInput = document.getElementById(
|
||||||
|
"modelHelperSearchInput"
|
||||||
|
);
|
||||||
|
const modelHelperListContainer = document.getElementById(
|
||||||
|
"modelHelperListContainer"
|
||||||
|
);
|
||||||
|
const closeModelHelperModalBtn = document.getElementById(
|
||||||
|
"closeModelHelperModalBtn"
|
||||||
|
);
|
||||||
|
const cancelModelHelperBtn = document.getElementById("cancelModelHelperBtn");
|
||||||
|
|
||||||
|
let cachedModelsList = null;
|
||||||
|
let currentModelHelperTarget = null; // { type: 'input'/'array', target: elementOrIdOrKey }
|
||||||
|
|
||||||
// Modal Control Functions
|
// Modal Control Functions
|
||||||
function openModal(modalElement) {
|
function openModal(modalElement) {
|
||||||
if (modalElement) {
|
if (modalElement) {
|
||||||
@@ -227,6 +244,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
bulkDeleteApiKeyModal,
|
bulkDeleteApiKeyModal,
|
||||||
proxyModal,
|
proxyModal,
|
||||||
bulkDeleteProxyModal,
|
bulkDeleteProxyModal,
|
||||||
|
modelHelperModal,
|
||||||
];
|
];
|
||||||
modals.forEach((modal) => {
|
modals.forEach((modal) => {
|
||||||
if (event.target === modal) {
|
if (event.target === modal) {
|
||||||
@@ -353,6 +371,44 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initializeSensitiveFields(); // Initialize sensitive field handling
|
initializeSensitiveFields(); // Initialize sensitive field handling
|
||||||
|
|
||||||
|
// Model Helper Modal Event Listeners
|
||||||
|
if (closeModelHelperModalBtn) {
|
||||||
|
closeModelHelperModalBtn.addEventListener("click", () =>
|
||||||
|
closeModal(modelHelperModal)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (cancelModelHelperBtn) {
|
||||||
|
cancelModelHelperBtn.addEventListener("click", () =>
|
||||||
|
closeModal(modelHelperModal)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (modelHelperSearchInput) {
|
||||||
|
modelHelperSearchInput.addEventListener("input", () =>
|
||||||
|
renderModelsInModal()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listeners to all model helper trigger buttons
|
||||||
|
const modelHelperTriggerBtns = document.querySelectorAll(
|
||||||
|
".model-helper-trigger-btn"
|
||||||
|
);
|
||||||
|
modelHelperTriggerBtns.forEach((btn) => {
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
const targetInputId = btn.dataset.targetInputId;
|
||||||
|
const targetArrayKey = btn.dataset.targetArrayKey;
|
||||||
|
|
||||||
|
if (targetInputId) {
|
||||||
|
currentModelHelperTarget = {
|
||||||
|
type: "input",
|
||||||
|
target: document.getElementById(targetInputId),
|
||||||
|
};
|
||||||
|
} else if (targetArrayKey) {
|
||||||
|
currentModelHelperTarget = { type: "array", targetKey: targetArrayKey };
|
||||||
|
}
|
||||||
|
openModelHelperModal();
|
||||||
|
});
|
||||||
|
});
|
||||||
}); // <-- DOMContentLoaded end
|
}); // <-- DOMContentLoaded end
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1719,3 +1775,140 @@ function addSafetySettingItem(category = "", threshold = "") {
|
|||||||
|
|
||||||
container.appendChild(settingItem);
|
container.appendChild(settingItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Model Helper Functions ---
|
||||||
|
async function fetchModels() {
|
||||||
|
if (cachedModelsList) {
|
||||||
|
return cachedModelsList;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
showNotification("正在从 /api/config/ui/models 加载模型列表...", "info");
|
||||||
|
const response = await fetch("/api/config/ui/models");
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.text();
|
||||||
|
throw new Error(`HTTP error ${response.status}: ${errorData}`);
|
||||||
|
}
|
||||||
|
const responseData = await response.json(); // Changed variable name to responseData
|
||||||
|
// The backend returns an object like: { object: "list", data: [{id: "m1"}, {id: "m2"}], success: true }
|
||||||
|
if (
|
||||||
|
responseData &&
|
||||||
|
responseData.success &&
|
||||||
|
Array.isArray(responseData.data)
|
||||||
|
) {
|
||||||
|
cachedModelsList = responseData.data; // Use responseData.data
|
||||||
|
showNotification("模型列表加载成功", "success");
|
||||||
|
return cachedModelsList;
|
||||||
|
} else {
|
||||||
|
console.error("Invalid model list format received:", responseData);
|
||||||
|
throw new Error("模型列表格式无效或请求未成功");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("加载模型列表失败:", error);
|
||||||
|
showNotification(`加载模型列表失败: ${error.message}`, "error");
|
||||||
|
cachedModelsList = []; // Avoid repeated fetches on error for this session, or set to null to retry
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderModelsInModal() {
|
||||||
|
if (!modelHelperListContainer) return;
|
||||||
|
if (!cachedModelsList) {
|
||||||
|
modelHelperListContainer.innerHTML =
|
||||||
|
'<p class="text-gray-400 text-sm italic">模型列表尚未加载。</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchTerm = modelHelperSearchInput.value.toLowerCase();
|
||||||
|
const filteredModels = cachedModelsList.filter((model) =>
|
||||||
|
model.id.toLowerCase().includes(searchTerm)
|
||||||
|
);
|
||||||
|
|
||||||
|
modelHelperListContainer.innerHTML = ""; // Clear previous items
|
||||||
|
|
||||||
|
if (filteredModels.length === 0) {
|
||||||
|
modelHelperListContainer.innerHTML =
|
||||||
|
'<p class="text-gray-400 text-sm italic">未找到匹配的模型。</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredModels.forEach((model) => {
|
||||||
|
const modelItemElement = document.createElement("button");
|
||||||
|
modelItemElement.type = "button";
|
||||||
|
modelItemElement.textContent = model.id;
|
||||||
|
modelItemElement.className =
|
||||||
|
"block w-full text-left px-4 py-2 rounded-md hover:bg-violet-700 focus:bg-violet-700 focus:outline-none transition-colors text-gray-200";
|
||||||
|
// Add any other classes for styling, e.g., from existing modals or array items
|
||||||
|
|
||||||
|
modelItemElement.addEventListener("click", () =>
|
||||||
|
handleModelSelection(model.id)
|
||||||
|
);
|
||||||
|
modelHelperListContainer.appendChild(modelItemElement);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openModelHelperModal() {
|
||||||
|
if (!currentModelHelperTarget) {
|
||||||
|
console.error("Model helper target not set.");
|
||||||
|
showNotification("无法打开模型助手:目标未设置", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetchModels(); // Ensure models are loaded
|
||||||
|
renderModelsInModal(); // Render them (handles empty/error cases internally)
|
||||||
|
|
||||||
|
if (modelHelperTitleElement) {
|
||||||
|
if (
|
||||||
|
currentModelHelperTarget.type === "input" &&
|
||||||
|
currentModelHelperTarget.target
|
||||||
|
) {
|
||||||
|
const label = document.querySelector(
|
||||||
|
`label[for="${currentModelHelperTarget.target.id}"]`
|
||||||
|
);
|
||||||
|
modelHelperTitleElement.textContent = label
|
||||||
|
? `为 "${label.textContent.trim()}" 选择模型`
|
||||||
|
: "选择模型";
|
||||||
|
} else if (currentModelHelperTarget.type === "array") {
|
||||||
|
modelHelperTitleElement.textContent = `为 ${currentModelHelperTarget.targetKey} 添加模型`;
|
||||||
|
} else {
|
||||||
|
modelHelperTitleElement.textContent = "选择模型";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (modelHelperSearchInput) modelHelperSearchInput.value = ""; // Clear search on open
|
||||||
|
if (modelHelperModal) openModal(modelHelperModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleModelSelection(selectedModelId) {
|
||||||
|
if (!currentModelHelperTarget) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentModelHelperTarget.type === "input" &&
|
||||||
|
currentModelHelperTarget.target
|
||||||
|
) {
|
||||||
|
const inputElement = currentModelHelperTarget.target;
|
||||||
|
inputElement.value = selectedModelId;
|
||||||
|
// If the input is a sensitive field, dispatch focusout to trigger masking behavior if needed
|
||||||
|
if (inputElement.classList.contains(SENSITIVE_INPUT_CLASS)) {
|
||||||
|
const event = new Event("focusout", { bubbles: true, cancelable: true });
|
||||||
|
inputElement.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
// Dispatch input event for any other listeners
|
||||||
|
inputElement.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
} else if (
|
||||||
|
currentModelHelperTarget.type === "array" &&
|
||||||
|
currentModelHelperTarget.targetKey
|
||||||
|
) {
|
||||||
|
const modelId = addArrayItemWithValue(
|
||||||
|
currentModelHelperTarget.targetKey,
|
||||||
|
selectedModelId
|
||||||
|
);
|
||||||
|
if (currentModelHelperTarget.targetKey === "THINKING_MODELS" && modelId) {
|
||||||
|
// Automatically add corresponding budget map item with default budget 0
|
||||||
|
createAndAppendBudgetMapItem(selectedModelId, 0, modelId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelHelperModal) closeModal(modelHelperModal);
|
||||||
|
currentModelHelperTarget = null; // Reset target
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- End Model Helper Functions --
|
||||||
|
|||||||
@@ -557,13 +557,23 @@ endblock %} {% block head_extra_styles %}
|
|||||||
<label for="TEST_MODEL" class="block font-semibold mb-2 text-gray-700"
|
<label for="TEST_MODEL" class="block font-semibold mb-2 text-gray-700"
|
||||||
>测试模型</label
|
>测试模型</label
|
||||||
>
|
>
|
||||||
<input
|
<div class="flex items-center gap-2">
|
||||||
type="text"
|
<input
|
||||||
id="TEST_MODEL"
|
type="text"
|
||||||
name="TEST_MODEL"
|
id="TEST_MODEL"
|
||||||
placeholder="gemini-1.5-flash"
|
name="TEST_MODEL"
|
||||||
class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 form-input-themed"
|
placeholder="gemini-1.5-flash"
|
||||||
/>
|
class="flex-grow px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 form-input-themed"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="选择模型"
|
||||||
|
class="model-helper-trigger-btn p-2 rounded-md text-violet-300 hover:bg-violet-700 transition-colors"
|
||||||
|
data-target-input-id="TEST_MODEL"
|
||||||
|
>
|
||||||
|
<i class="fas fa-list-ul"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<small class="text-gray-500 mt-1 block">用于测试API密钥的模型</small>
|
<small class="text-gray-500 mt-1 block">用于测试API密钥的模型</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -577,7 +587,15 @@ endblock %} {% block head_extra_styles %}
|
|||||||
<div class="array-container" id="IMAGE_MODELS_container">
|
<div class="array-container" id="IMAGE_MODELS_container">
|
||||||
<!-- 数组项将在这里动态添加 -->
|
<!-- 数组项将在这里动态添加 -->
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end gap-2 mt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="从列表选择模型添加到下方"
|
||||||
|
class="model-helper-trigger-btn p-2 rounded-md text-violet-300 hover:bg-violet-700 transition-colors"
|
||||||
|
data-target-array-key="IMAGE_MODELS"
|
||||||
|
>
|
||||||
|
<i class="fas fa-list-ul"></i>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2"
|
class="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2"
|
||||||
@@ -599,7 +617,15 @@ endblock %} {% block head_extra_styles %}
|
|||||||
<div class="array-container" id="SEARCH_MODELS_container">
|
<div class="array-container" id="SEARCH_MODELS_container">
|
||||||
<!-- 数组项将在这里动态添加 -->
|
<!-- 数组项将在这里动态添加 -->
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end gap-2 mt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="从列表选择模型添加到下方"
|
||||||
|
class="model-helper-trigger-btn p-2 rounded-md text-violet-300 hover:bg-violet-700 transition-colors"
|
||||||
|
data-target-array-key="SEARCH_MODELS"
|
||||||
|
>
|
||||||
|
<i class="fas fa-list-ul"></i>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2"
|
class="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2"
|
||||||
@@ -621,7 +647,15 @@ endblock %} {% block head_extra_styles %}
|
|||||||
<div class="array-container" id="FILTERED_MODELS_container">
|
<div class="array-container" id="FILTERED_MODELS_container">
|
||||||
<!-- 数组项将在这里动态添加 -->
|
<!-- 数组项将在这里动态添加 -->
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end gap-2 mt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="从列表选择模型添加到下方"
|
||||||
|
class="model-helper-trigger-btn p-2 rounded-md text-violet-300 hover:bg-violet-700 transition-colors"
|
||||||
|
data-target-array-key="FILTERED_MODELS"
|
||||||
|
>
|
||||||
|
<i class="fas fa-list-ul"></i>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2"
|
class="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2"
|
||||||
@@ -708,7 +742,15 @@ endblock %} {% block head_extra_styles %}
|
|||||||
<div class="array-container" id="THINKING_MODELS_container">
|
<div class="array-container" id="THINKING_MODELS_container">
|
||||||
<!-- 数组项将在这里动态添加 -->
|
<!-- 数组项将在这里动态添加 -->
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end gap-2 mt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="从列表选择模型添加到下方"
|
||||||
|
class="model-helper-trigger-btn p-2 rounded-md text-violet-300 hover:bg-violet-700 transition-colors"
|
||||||
|
data-target-array-key="THINKING_MODELS"
|
||||||
|
>
|
||||||
|
<i class="fas fa-list-ul"></i>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2"
|
class="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2"
|
||||||
@@ -829,13 +871,23 @@ endblock %} {% block head_extra_styles %}
|
|||||||
class="block font-semibold mb-2 text-gray-700"
|
class="block font-semibold mb-2 text-gray-700"
|
||||||
>图像生成模型</label
|
>图像生成模型</label
|
||||||
>
|
>
|
||||||
<input
|
<div class="flex items-center gap-2">
|
||||||
type="text"
|
<input
|
||||||
id="CREATE_IMAGE_MODEL"
|
type="text"
|
||||||
name="CREATE_IMAGE_MODEL"
|
id="CREATE_IMAGE_MODEL"
|
||||||
placeholder="imagen-3.0-generate-002"
|
name="CREATE_IMAGE_MODEL"
|
||||||
class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 form-input-themed"
|
placeholder="imagen-3.0-generate-002"
|
||||||
/>
|
class="flex-grow px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 form-input-themed"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="选择模型"
|
||||||
|
class="model-helper-trigger-btn p-2 rounded-md text-violet-300 hover:bg-violet-700 transition-colors"
|
||||||
|
data-target-input-id="CREATE_IMAGE_MODEL"
|
||||||
|
>
|
||||||
|
<i class="fas fa-list-ul"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<small class="text-gray-500 mt-1 block">用于图像生成的模型</small>
|
<small class="text-gray-500 mt-1 block">用于图像生成的模型</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1505,6 +1557,55 @@ endblock %} {% block head_extra_styles %}
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Model Helper Modal -->
|
||||||
|
<div id="modelHelperModal" class="modal">
|
||||||
|
<div
|
||||||
|
class="w-full max-w-lg mx-auto rounded-2xl shadow-2xl overflow-hidden animate-fade-in"
|
||||||
|
style="
|
||||||
|
background-color: rgba(70, 50, 150, 0.95);
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid rgba(120, 100, 200, 0.4);
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h2 id="modelHelperTitle" class="text-xl font-bold text-gray-100">
|
||||||
|
选择模型
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
id="closeModelHelperModalBtn"
|
||||||
|
class="text-gray-300 hover:text-gray-100 text-xl"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="modelHelperSearchInput"
|
||||||
|
placeholder="搜索模型..."
|
||||||
|
class="w-full px-4 py-3 mb-4 rounded-lg border font-mono text-sm form-input-themed"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
id="modelHelperListContainer"
|
||||||
|
class="array-container"
|
||||||
|
style="max-height: 300px; overflow-y: auto"
|
||||||
|
>
|
||||||
|
<!-- Model items will be populated here -->
|
||||||
|
<p class="text-gray-400 text-sm italic">正在加载模型列表...</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-3 mt-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="cancelModelHelperBtn"
|
||||||
|
class="bg-gray-500 bg-opacity-50 hover:bg-opacity-70 text-gray-200 px-6 py-2 rounded-lg font-medium transition"
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %} {% block body_scripts %}
|
{% endblock %} {% block body_scripts %}
|
||||||
<script src="/static/js/config_editor.js"></script>
|
<script src="/static/js/config_editor.js"></script>
|
||||||
<!-- 增强下拉框样式和交互性 -->
|
<!-- 增强下拉框样式和交互性 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user