feat: 完成T-501b设置页角色与生成参数
- 在SettingsTab增加标题/图片默认模型角色下拉并按text/image过滤 - 增加并发、重试、分辨率、返回超时展示和jpg质量设置 - 增加Chrome路径、账号数据根目录、图片目录、DB路径和端口配置 - 保存到config.json并校验端口范围,避免写入测试注入路径 - 补充GUI测试并同步任务、api、routes、current-state和progress
This commit is contained in:
+245
-2
@@ -26,6 +26,7 @@ try:
|
||||
QMessageBox,
|
||||
QPlainTextEdit,
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
QSplitter,
|
||||
QTableView,
|
||||
QSpinBox,
|
||||
@@ -2437,16 +2438,23 @@ if QT_IMPORT_ERROR is None:
|
||||
|
||||
CATEGORY_ITEMS = [("文本", "text"), ("图像", "image")]
|
||||
API_TYPE_ITEMS = [("chat", "chat"), ("images_edits", "images_edits"), ("auto", "auto")]
|
||||
RESOLUTION_ITEMS = ["512", "1k", "2k", "4k"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
config=None,
|
||||
config_path=None,
|
||||
ai_models_path=None,
|
||||
status_callback=None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.config = appconfig.load_config() if config is None else config
|
||||
self.config_path = (
|
||||
config_path
|
||||
or self.config.get("config_path")
|
||||
or appconfig.CONFIG_PATH
|
||||
)
|
||||
self.ai_models_path = (
|
||||
ai_models_path
|
||||
or self.config.get("ai_models_path")
|
||||
@@ -2501,6 +2509,48 @@ if QT_IMPORT_ERROR is None:
|
||||
self.test_connection_button = QPushButton("测试连接")
|
||||
self.test_result_label = QLabel("")
|
||||
self.test_result_label.setWordWrap(True)
|
||||
self.default_text_model_combo = QComboBox()
|
||||
self.default_text_model_combo.setObjectName("defaultTextModelCombo")
|
||||
self.default_image_model_combo = QComboBox()
|
||||
self.default_image_model_combo.setObjectName("defaultImageModelCombo")
|
||||
self.title_concurrency_spin = QSpinBox()
|
||||
self.title_concurrency_spin.setObjectName("titleConcurrencySpin")
|
||||
self.title_concurrency_spin.setRange(1, 64)
|
||||
self.image_concurrency_spin = QSpinBox()
|
||||
self.image_concurrency_spin.setObjectName("imageConcurrencySpin")
|
||||
self.image_concurrency_spin.setRange(1, 64)
|
||||
self.retry_spin = QSpinBox()
|
||||
self.retry_spin.setObjectName("retrySpin")
|
||||
self.retry_spin.setRange(0, 20)
|
||||
self.resolution_combo = QComboBox()
|
||||
self.resolution_combo.setObjectName("resolutionCombo")
|
||||
for resolution in self.RESOLUTION_ITEMS:
|
||||
self.resolution_combo.addItem(resolution, resolution)
|
||||
self.response_timeout_label = QLabel("")
|
||||
self.jpg_quality_spin = QSpinBox()
|
||||
self.jpg_quality_spin.setObjectName("jpgQualitySpin")
|
||||
self.jpg_quality_spin.setRange(1, 100)
|
||||
self.chrome_path_edit = QLineEdit()
|
||||
self.chrome_path_edit.setObjectName("chromePathEdit")
|
||||
self.user_data_root_edit = QLineEdit()
|
||||
self.user_data_root_edit.setObjectName("userDataRootEdit")
|
||||
self.image_dir_edit = QLineEdit()
|
||||
self.image_dir_edit.setObjectName("imageDirEdit")
|
||||
self.db_path_edit = QLineEdit()
|
||||
self.db_path_edit.setObjectName("dbPathEdit")
|
||||
self.default_debug_port_spin = QSpinBox()
|
||||
self.default_debug_port_spin.setObjectName("defaultDebugPortSpin")
|
||||
self.default_debug_port_spin.setRange(1, 65535)
|
||||
self.debug_port_start_spin = QSpinBox()
|
||||
self.debug_port_start_spin.setObjectName("debugPortStartSpin")
|
||||
self.debug_port_start_spin.setRange(1, 65535)
|
||||
self.debug_port_end_spin = QSpinBox()
|
||||
self.debug_port_end_spin.setObjectName("debugPortEndSpin")
|
||||
self.debug_port_end_spin.setRange(1, 65535)
|
||||
self.cdp_ready_timeout_spin = QSpinBox()
|
||||
self.cdp_ready_timeout_spin.setObjectName("cdpReadyTimeoutSpin")
|
||||
self.cdp_ready_timeout_spin.setRange(1, 3600)
|
||||
self.save_config_button = QPushButton("保存设置")
|
||||
|
||||
form = QFormLayout()
|
||||
form.addRow("", self.enabled_checkbox)
|
||||
@@ -2517,6 +2567,30 @@ if QT_IMPORT_ERROR is None:
|
||||
action_layout.addWidget(self.test_connection_button)
|
||||
action_layout.addStretch(1)
|
||||
|
||||
ai_form = QFormLayout()
|
||||
ai_form.addRow("标题大模型", self.default_text_model_combo)
|
||||
ai_form.addRow("图片大模型", self.default_image_model_combo)
|
||||
ai_form.addRow("标题并发数", self.title_concurrency_spin)
|
||||
ai_form.addRow("图片并发数", self.image_concurrency_spin)
|
||||
ai_form.addRow("失败重试次数", self.retry_spin)
|
||||
ai_form.addRow("分辨率", self.resolution_combo)
|
||||
ai_form.addRow("返回超时", self.response_timeout_label)
|
||||
ai_form.addRow("jpg质量", self.jpg_quality_spin)
|
||||
|
||||
port_range_layout = QHBoxLayout()
|
||||
port_range_layout.addWidget(self.debug_port_start_spin)
|
||||
port_range_layout.addWidget(QLabel("到"))
|
||||
port_range_layout.addWidget(self.debug_port_end_spin)
|
||||
|
||||
path_form = QFormLayout()
|
||||
path_form.addRow("Chrome路径", self.chrome_path_edit)
|
||||
path_form.addRow("账号数据根目录", self.user_data_root_edit)
|
||||
path_form.addRow("图片目录", self.image_dir_edit)
|
||||
path_form.addRow("DB路径", self.db_path_edit)
|
||||
path_form.addRow("默认调试端口", self.default_debug_port_spin)
|
||||
path_form.addRow("调试端口范围", port_range_layout)
|
||||
path_form.addRow("CDP就绪超时(秒)", self.cdp_ready_timeout_spin)
|
||||
|
||||
right_panel = QWidget()
|
||||
right_layout = QVBoxLayout(right_panel)
|
||||
right_layout.setContentsMargins(12, 0, 0, 0)
|
||||
@@ -2524,11 +2598,22 @@ if QT_IMPORT_ERROR is None:
|
||||
right_layout.addLayout(form)
|
||||
right_layout.addLayout(action_layout)
|
||||
right_layout.addWidget(self.test_result_label)
|
||||
right_layout.addSpacing(18)
|
||||
right_layout.addWidget(QLabel("角色与生成参数"))
|
||||
right_layout.addLayout(ai_form)
|
||||
right_layout.addSpacing(18)
|
||||
right_layout.addWidget(QLabel("路径与端口"))
|
||||
right_layout.addLayout(path_form)
|
||||
right_layout.addWidget(self.save_config_button)
|
||||
right_layout.addStretch(1)
|
||||
|
||||
right_scroll = QScrollArea()
|
||||
right_scroll.setWidgetResizable(True)
|
||||
right_scroll.setWidget(right_panel)
|
||||
|
||||
self.splitter = QSplitter(Qt.Horizontal)
|
||||
self.splitter.addWidget(left_panel)
|
||||
self.splitter.addWidget(right_panel)
|
||||
self.splitter.addWidget(right_scroll)
|
||||
self.splitter.setStretchFactor(0, 1)
|
||||
self.splitter.setStretchFactor(1, 3)
|
||||
self.splitter.setSizes([280, 860])
|
||||
@@ -2542,8 +2627,13 @@ if QT_IMPORT_ERROR is None:
|
||||
self.delete_model_button.clicked.connect(self.delete_model)
|
||||
self.save_model_button.clicked.connect(self.save_model)
|
||||
self.test_connection_button.clicked.connect(self.test_connection)
|
||||
self.resolution_combo.currentIndexChanged.connect(
|
||||
self._update_response_timeout_label
|
||||
)
|
||||
self.save_config_button.clicked.connect(self.save_app_settings)
|
||||
|
||||
self.refresh_models()
|
||||
self._populate_app_settings()
|
||||
|
||||
def _set_status(self, message):
|
||||
if self.status_callback is not None:
|
||||
@@ -2572,6 +2662,8 @@ if QT_IMPORT_ERROR is None:
|
||||
self.model_combo.setCurrentIndex(index if index >= 0 else (0 if self.models else -1))
|
||||
self.model_combo.blockSignals(False)
|
||||
self.load_selected_model()
|
||||
if hasattr(self, "default_text_model_combo"):
|
||||
self._populate_role_model_combos()
|
||||
|
||||
def load_selected_model(self, index=None):
|
||||
name = self.model_combo.currentData()
|
||||
@@ -2667,6 +2759,151 @@ if QT_IMPORT_ERROR is None:
|
||||
self._set_status(f"正在测试 AI 模型连接:{model['name']}")
|
||||
thread.start()
|
||||
|
||||
def save_app_settings(self, checked=False):
|
||||
settings = self._app_settings_values()
|
||||
if settings is None:
|
||||
return
|
||||
try:
|
||||
saved = appconfig.save_config(settings, path=self.config_path)
|
||||
except Exception as exc:
|
||||
self._show_error(exc)
|
||||
return
|
||||
self._replace_config(saved)
|
||||
self._populate_app_settings()
|
||||
self._set_status("设置已保存")
|
||||
|
||||
def _app_settings_values(self):
|
||||
start_port = self.debug_port_start_spin.value()
|
||||
end_port = self.debug_port_end_spin.value()
|
||||
default_port = self.default_debug_port_spin.value()
|
||||
if start_port > end_port:
|
||||
self._show_error("调试端口范围起始值不能大于结束值")
|
||||
return None
|
||||
if not (start_port <= default_port <= end_port):
|
||||
self._show_error("默认调试端口必须在调试端口范围内")
|
||||
return None
|
||||
text_model = self.default_text_model_combo.currentData()
|
||||
image_model = self.default_image_model_combo.currentData()
|
||||
if not text_model or not image_model:
|
||||
self._show_error("标题大模型和图片大模型不能为空")
|
||||
return None
|
||||
|
||||
ai_cfg = appconfig.ai_config(self.config)
|
||||
ai_cfg.update(
|
||||
{
|
||||
"default_text_model": text_model,
|
||||
"default_image_model": image_model,
|
||||
"title_concurrency": self.title_concurrency_spin.value(),
|
||||
"image_concurrency": self.image_concurrency_spin.value(),
|
||||
"retry": self.retry_spin.value(),
|
||||
"jpg_quality": self.jpg_quality_spin.value(),
|
||||
"resolution": self.resolution_combo.currentData() or "1k",
|
||||
"resolution_timeouts": dict(ai_cfg.get("resolution_timeouts", {})),
|
||||
}
|
||||
)
|
||||
|
||||
settings = {
|
||||
key: value
|
||||
for key, value in self.config.items()
|
||||
if key not in {"config_path", "ai_models_path"}
|
||||
}
|
||||
settings.update(
|
||||
{
|
||||
"chrome_path": self.chrome_path_edit.text().strip(),
|
||||
"user_data_root": self.user_data_root_edit.text().strip(),
|
||||
"image_dir": self.image_dir_edit.text().strip(),
|
||||
"db_path": self.db_path_edit.text().strip(),
|
||||
"default_debug_port": default_port,
|
||||
"debug_port_range": [start_port, end_port],
|
||||
"cdp_ready_timeout": self.cdp_ready_timeout_spin.value(),
|
||||
"ai": ai_cfg,
|
||||
}
|
||||
)
|
||||
return settings
|
||||
|
||||
def _replace_config(self, saved):
|
||||
internal = {}
|
||||
if self.config_path != appconfig.CONFIG_PATH:
|
||||
internal["config_path"] = self.config_path
|
||||
if self.ai_models_path != appconfig.AI_MODELS_PATH:
|
||||
internal["ai_models_path"] = self.ai_models_path
|
||||
self.config.clear()
|
||||
self.config.update(saved)
|
||||
self.config.update(internal)
|
||||
|
||||
def _populate_app_settings(self):
|
||||
self._populate_role_model_combos()
|
||||
ai_cfg = appconfig.ai_config(self.config)
|
||||
self._set_combo_by_data(
|
||||
self.default_text_model_combo,
|
||||
ai_cfg.get("default_text_model", ""),
|
||||
)
|
||||
self._set_combo_by_data(
|
||||
self.default_image_model_combo,
|
||||
ai_cfg.get("default_image_model", ""),
|
||||
)
|
||||
self.title_concurrency_spin.setValue(
|
||||
int(ai_cfg.get("title_concurrency", 4) or 4)
|
||||
)
|
||||
self.image_concurrency_spin.setValue(
|
||||
int(ai_cfg.get("image_concurrency", 4) or 4)
|
||||
)
|
||||
self.retry_spin.setValue(int(ai_cfg.get("retry", 2) or 0))
|
||||
self._set_combo_by_data(
|
||||
self.resolution_combo,
|
||||
str(ai_cfg.get("resolution", "1k")),
|
||||
)
|
||||
self.jpg_quality_spin.setValue(int(ai_cfg.get("jpg_quality", 90) or 90))
|
||||
self.chrome_path_edit.setText(appconfig.chrome_path(self.config))
|
||||
self.user_data_root_edit.setText(appconfig.user_data_root(self.config))
|
||||
self.image_dir_edit.setText(appconfig.image_dir(self.config))
|
||||
self.db_path_edit.setText(appconfig.db_path(self.config))
|
||||
self.default_debug_port_spin.setValue(
|
||||
int(appconfig.default_debug_port(self.config))
|
||||
)
|
||||
start_port, end_port = appconfig.debug_port_range(self.config)
|
||||
self.debug_port_start_spin.setValue(int(start_port))
|
||||
self.debug_port_end_spin.setValue(int(end_port))
|
||||
self.cdp_ready_timeout_spin.setValue(
|
||||
int(appconfig.cdp_ready_timeout(self.config))
|
||||
)
|
||||
self._update_response_timeout_label()
|
||||
|
||||
def _populate_role_model_combos(self):
|
||||
ai_cfg = appconfig.ai_config(self.config)
|
||||
self._populate_role_combo(
|
||||
self.default_text_model_combo,
|
||||
"text",
|
||||
ai_cfg.get("default_text_model"),
|
||||
)
|
||||
self._populate_role_combo(
|
||||
self.default_image_model_combo,
|
||||
"image",
|
||||
ai_cfg.get("default_image_model"),
|
||||
)
|
||||
|
||||
def _populate_role_combo(self, combo, category, selected):
|
||||
combo.blockSignals(True)
|
||||
combo.clear()
|
||||
for model in self.models:
|
||||
if model.get("category") == category and model.get("enabled", True):
|
||||
combo.addItem(model.get("name", ""), model.get("name", ""))
|
||||
if combo.count() == 0:
|
||||
combo.addItem("无可用模型", None)
|
||||
index = combo.findData(selected)
|
||||
combo.setCurrentIndex(index if index >= 0 else 0)
|
||||
combo.blockSignals(False)
|
||||
|
||||
def _update_response_timeout_label(self, index=None):
|
||||
ai_cfg = appconfig.ai_config(self.config)
|
||||
resolution = self.resolution_combo.currentData() or ai_cfg.get("resolution", "1k")
|
||||
timeouts = ai_cfg.get("resolution_timeouts", {})
|
||||
timeout = timeouts.get(str(resolution))
|
||||
if timeout is None:
|
||||
self.response_timeout_label.setText("未配置")
|
||||
return
|
||||
self.response_timeout_label.setText(f"{int(timeout)} 秒")
|
||||
|
||||
def _form_values(self):
|
||||
current = self._current_model() or {}
|
||||
name = self.name_edit.text().strip()
|
||||
@@ -3069,10 +3306,15 @@ if QT_IMPORT_ERROR is None:
|
||||
class MainWindow(QMainWindow):
|
||||
"""Main application window with the fixed five-tab workflow."""
|
||||
|
||||
def __init__(self, db_path=None, config=None, ai_models_path=None):
|
||||
def __init__(self, db_path=None, config=None, config_path=None, ai_models_path=None):
|
||||
super().__init__()
|
||||
self.config = appconfig.load_config() if config is None else config
|
||||
self.db_path = _database_path(db_path, self.config)
|
||||
self.config_path = (
|
||||
config_path
|
||||
or self.config.get("config_path")
|
||||
or appconfig.CONFIG_PATH
|
||||
)
|
||||
self.ai_models_path = (
|
||||
ai_models_path
|
||||
or self.config.get("ai_models_path")
|
||||
@@ -3118,6 +3360,7 @@ if QT_IMPORT_ERROR is None:
|
||||
)
|
||||
return SettingsTab(
|
||||
config=self.config,
|
||||
config_path=self.config_path,
|
||||
ai_models_path=self.ai_models_path,
|
||||
status_callback=self.statusBar().showMessage,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user