feat: 完成T-501b设置页角色与生成参数

- 在SettingsTab增加标题/图片默认模型角色下拉并按text/image过滤

- 增加并发、重试、分辨率、返回超时展示和jpg质量设置

- 增加Chrome路径、账号数据根目录、图片目录、DB路径和端口配置

- 保存到config.json并校验端口范围,避免写入测试注入路径

- 补充GUI测试并同步任务、api、routes、current-state和progress
This commit is contained in:
chengma
2026-06-29 08:58:39 +08:00
parent bf961e62d6
commit 91683abe7d
7 changed files with 424 additions and 17 deletions
+245 -2
View File
@@ -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,
)