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,
|
||||
)
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@
|
||||
| ID | 任务 | 依赖 | 验收要点 | 状态 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| T-501 | Tab⑤ 设置 · AI 模型管理 UI(master-detail):模型下拉 + 新增/删除 + 详情(服务商名/类别/api_type/模型ID/网址/密钥打码/连接超时) + 测试连接 | T-005, T-104 | 复用 T-005 后端;name 唯一;类别 text/image;至少各一个;密钥本地明文保存、UI 打码;测试连接可用 | DONE |
|
||||
| T-501b | Tab⑤ 设置 · 角色与生成参数:标题大模型/图片大模型下拉、分辨率(512/1k/2k/4k)、返回超时随分辨率、并发/重试/jpg质量、路径/端口 | T-501 | 角色下拉按类别过滤;分辨率选后返回超时按 resolution_timeouts 自动;持久化 config.json | TODO |
|
||||
| T-501b | Tab⑤ 设置 · 角色与生成参数:标题大模型/图片大模型下拉、分辨率(512/1k/2k/4k)、返回超时随分辨率、并发/重试/jpg质量、路径/端口 | T-501 | 角色下拉按类别过滤;分辨率选后返回超时按 resolution_timeouts 自动;持久化 config.json | DONE |
|
||||
| T-501c | Tab⑤ 设置 · Shopee 更新安全开关 | T-501b | 配置测试商品 ID、是否允许真实提交、是否允许更新封面、单次最大更新条数、成功后是否自动关闭本次新开编辑 tab;默认关闭真实提交和封面更新;Tab③ 执行前读取并拦截不符合安全配置的更新 | TODO |
|
||||
| T-502 | 满 9 张封面:删第一张再上传 | T-001 | 实测删除确认框并处理;满 9 张流程正确 | TODO |
|
||||
| T-503 | 敏感信息本地明文存储提示与日志脱敏 | T-105, T-501 | 首次保存密码/API Key 时提示“本地明文保存”;UI 打码;日志/导出不含密码/API Key;文档说明 `config.json`/`config/ai_models.json`/DB/user-data-dir/images 必须 gitignore | TODO |
|
||||
|
||||
+5
-2
@@ -293,7 +293,7 @@ render_prompt(template_text, task) -> str
|
||||
```python
|
||||
# GUI 入口
|
||||
main() -> int # 创建 QApplication + MainWindow
|
||||
class MainWindow(QMainWindow) # QTabWidget: ①②③④⑤;支持注入 db_path/config/ai_models_path 便于测试
|
||||
class MainWindow(QMainWindow) # QTabWidget: ①②③④⑤;支持注入 db_path/config/config_path/ai_models_path 便于测试
|
||||
class CollectTab(QWidget) # ① 导入采集:导入 Excel + 汇总栏 + QTableView 任务列表 + 未匹配略过标记
|
||||
class GenerateTab(QWidget) # ② AI生成:提示词管理 + 筛选任务 + 开始/停止生成 + 新旧封面预览
|
||||
class ApplyTab(QWidget) # ③ 更新shopee:筛选已生成任务 + 确认后串行执行更新
|
||||
@@ -326,10 +326,13 @@ TAB_STYLE: str # 顶层 Tab 栏防误点样式:
|
||||
|
||||
- `SettingsTab` 左侧为 AI 模型下拉与新增/删除按钮,右侧为模型详情表单。
|
||||
- 详情字段:启用、服务商名、类别(text/image)、api_type、模型 ID、网址、密钥、连接超时。
|
||||
- 模型数据读写复用 `appconfig.list_ai_models(..., reveal_api_key=True)`、`add_ai_model()`、`update_ai_model()`、`delete_ai_model()`;保存时保留现有 `extra_body` 与 `timeout_seconds`,不接管 T-501b 的生成参数。
|
||||
- 模型数据读写复用 `appconfig.list_ai_models(..., reveal_api_key=True)`、`add_ai_model()`、`update_ai_model()`、`delete_ai_model()`;保存时保留现有 `extra_body` 与 `timeout_seconds`。
|
||||
- 密钥字段使用 `QLineEdit.Password` 打码显示;明文只写入已 gitignore 的 `config/ai_models.json`,不得进入日志/导出。
|
||||
- 删除按钮在当前类别只剩 1 个模型时禁用;后端仍以“至少启用一个 text 和 image 模型”为硬约束。
|
||||
- 「测试连接」创建 `AIModelTestWorker` 后台调用 `appconfig.test_ai_model()`,GUI 主线程不直接发网络请求。
|
||||
- 角色与生成参数读写 `config.json`:标题大模型(仅 text)、图片大模型(仅 image)、标题/图片并发、失败重试、分辨率、jpg 质量。
|
||||
- 分辨率下拉固定 `512/1k/2k/4k`;返回超时标签只读展示 `resolution_timeouts[resolution]`。
|
||||
- 路径与端口读写 `config.json`:Chrome 路径、账号数据根目录、图片目录、DB 路径、默认调试端口、调试端口范围、CDP 就绪超时。保存时校验端口范围和默认端口。
|
||||
|
||||
① 导入采集当前要点(T-202/T-202b):
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
## 当前快照
|
||||
|
||||
- 日期:2026-06-27
|
||||
- 阶段:V0 单账号 CDP 流程已验证;V1 已完成 T-000 正式代码包结构、T-001 `app/editor.py` 模块化、T-002 `app/appconfig.py` 应用配置、T-003 SQLite 持久化地基、T-004 本地数据忽略规则、T-005 AI 模型清单后端、T-006 单元测试基座、T-101 账号 user-data-dir 工具、T-102 Chrome 启动器、T-103 登录保活与检测、T-104 PySide6 主窗口骨架、T-104b PySide6 worker 基座、T-105 Tab④ 账号管理、T-106 账号快捷方式、T-201 Excel 导入入库、T-202 Tab① 任务列表与导入按钮、T-202b Tab① 导入汇总栏、T-203 采集旧标题旧封面、T-204 回写旧字段到原 Excel、T-204b 采集完成自动回写、T-205 首次未配账号/Chrome 未启动/未登录引导保护、T-205b 采集后关闭自动新建商品页 tab、T-301 AI 生成接口、T-302 Tab② AI 生成布局与任务列表、T-302p 提示词管理、T-303 Tab② 开始生成/停止/进度、T-401 Tab③ 更新列表筛选与开始更新确认、T-402 Tab③ 确认后串行更新、T-403 Tab③ 结果回写与结束汇总、T-501 Tab⑤ AI 模型管理 UI。
|
||||
- 日期:2026-06-29
|
||||
- 阶段:V0 单账号 CDP 流程已验证;V1 已完成 T-000 正式代码包结构、T-001 `app/editor.py` 模块化、T-002 `app/appconfig.py` 应用配置、T-003 SQLite 持久化地基、T-004 本地数据忽略规则、T-005 AI 模型清单后端、T-006 单元测试基座、T-101 账号 user-data-dir 工具、T-102 Chrome 启动器、T-103 登录保活与检测、T-104 PySide6 主窗口骨架、T-104b PySide6 worker 基座、T-105 Tab④ 账号管理、T-106 账号快捷方式、T-201 Excel 导入入库、T-202 Tab① 任务列表与导入按钮、T-202b Tab① 导入汇总栏、T-203 采集旧标题旧封面、T-204 回写旧字段到原 Excel、T-204b 采集完成自动回写、T-205 首次未配账号/Chrome 未启动/未登录引导保护、T-205b 采集后关闭自动新建商品页 tab、T-301 AI 生成接口、T-302 Tab② AI 生成布局与任务列表、T-302p 提示词管理、T-303 Tab② 开始生成/停止/进度、T-401 Tab③ 更新列表筛选与开始更新确认、T-402 Tab③ 确认后串行更新、T-403 Tab③ 结果回写与结束汇总、T-501 Tab⑤ AI 模型管理 UI、T-501b Tab⑤ 角色与生成参数。
|
||||
- 技术栈:Python 3.10+,自研 CDP(websocket-client + requests),SQLite(sqlite3)+ `config.json` + openpyxl + AI(`config/ai_models.json` 通用 HTTP,chat JSON / images_edits),GUI PySide6 5 Tab(已定)。
|
||||
- 生产代码:已建立 `app/` 包 + 根入口 `main.py`;`app/cdp.py` 为已验证 CDP 底座,已区分 `CDP.close()` 断开 WebSocket 与 `close_tab()` 关闭浏览器 target;`app/editor.py` 已封装登录状态检测、标题/封面/采集/更新按钮能力,并在采集结束后只关闭本轮自动新建的商品编辑页 tab、保留用户已有 tab;`app/appconfig.py` 已实现 `config.json` 默认值/读写/更新、AI 参数与端口读取,以及 `config/ai_models.json` 模型清单 CRUD/过滤/打码/测试连接;`app/ai.py` 已实现 `gen_title()`/`gen_cover()`/`generate_batch()`,按默认文本/图片模型发起通用 HTTP 调用,支持重试、错误脱敏、图片 URL/base64 解析、resolution resize、jpg_quality 保存、先并发标题再并发封面、逐条 `set_generated`、失败 `mark_failed` 与停止取消未开始项;`app/prompts.py` 已实现标题提示词读写、封面模板 CRUD 与变量替换;`app/db.py` 已实现 SQLite schema、连接 PRAGMA、批次/账号/任务与阶段写库函数;`app/excel.py` 已实现多 Excel 输入列解析、整文件列校验、脏行统计跳过、导入批次与任务入库、别名匹配统计、旧标题/旧封面路径回写原 Excel、更新结果回写原 Excel 与另存副本;`app/config.py` 已实现账号 slug 与 user-data-dir 创建;`app/accounts.py` 已实现账号 CRUD 服务、端口默认分配、启动登录、检测登录、生成快捷方式;`app/chrome.py` 已实现 Chrome 参数拼装、启动、CDP 端口探测、PowerShell `.lnk` 快捷方式生成;`app/gui.py` 已实现 PySide6 `MainWindow`、五 Tab、顶部 Tab 栏防误点样式、① 导入采集的 Excel 导入按钮/导入汇总栏/QTableView 任务列表/未匹配筛选与略过标记/采集旧标题旧封面 worker/采集前账号就绪预检与④引导/采集完成自动回写/旧数据回写重试按钮与 worker、② AI生成左右布局/标题与封面提示词管理/批次店铺状态筛选/任务列表/变量预览/开始生成/停止/进度/双击新旧封面预览与 `GenerateWorker`、③ 更新shopee筛选栏/任务列表/开始更新确认弹窗/确认后 `ApplyWorker` 串行更新/账号就绪预检与④引导/逐条 `set_applied`/自动回写结果到 Excel/结束汇总弹窗/手动回写重试按钮、④ 账号管理表格/弹窗/按钮/快捷方式与状态栏、⑤ 设置 AI 模型 master-detail 管理与 `AIModelTestWorker` 后台测试连接;`app/workers.py` 已实现 `BaseWorker`、通用 signals、取消标记和 `QThread` 启动包装。
|
||||
- 测试:`tests/` 已建立;T-006 后纯逻辑改动必须运行 `python -m unittest discover -s tests`,当前覆盖 appconfig/db/config/accounts/chrome 启动与快捷方式/editor 登录检测与商品 tab 生命周期/excel 导入/旧字段与更新结果回写/ai 标题与封面 HTTP 解析/`generate_batch` 正常、失败与停止/prompts 读写与渲染/gui ① 导入采集/gui ② AI生成布局筛选、提示词管理、生成 worker 与双击预览/gui ③ 更新shopee筛选列表、确认弹窗、`ApplyWorker` 串行更新与账号预检、结果回写与汇总/gui ④ 账号管理/gui ⑤ AI 模型管理/worker signal 与线程包装;CDP/Shopee 改动仍需测试商品手动验证。
|
||||
- 生产代码:已建立 `app/` 包 + 根入口 `main.py`;`app/cdp.py` 为已验证 CDP 底座,已区分 `CDP.close()` 断开 WebSocket 与 `close_tab()` 关闭浏览器 target;`app/editor.py` 已封装登录状态检测、标题/封面/采集/更新按钮能力,并在采集结束后只关闭本轮自动新建的商品编辑页 tab、保留用户已有 tab;`app/appconfig.py` 已实现 `config.json` 默认值/读写/更新、AI 参数与端口读取,以及 `config/ai_models.json` 模型清单 CRUD/过滤/打码/测试连接;`app/ai.py` 已实现 `gen_title()`/`gen_cover()`/`generate_batch()`,按默认文本/图片模型发起通用 HTTP 调用,支持重试、错误脱敏、图片 URL/base64 解析、resolution resize、jpg_quality 保存、先并发标题再并发封面、逐条 `set_generated`、失败 `mark_failed` 与停止取消未开始项;`app/prompts.py` 已实现标题提示词读写、封面模板 CRUD 与变量替换;`app/db.py` 已实现 SQLite schema、连接 PRAGMA、批次/账号/任务与阶段写库函数;`app/excel.py` 已实现多 Excel 输入列解析、整文件列校验、脏行统计跳过、导入批次与任务入库、别名匹配统计、旧标题/旧封面路径回写原 Excel、更新结果回写原 Excel 与另存副本;`app/config.py` 已实现账号 slug 与 user-data-dir 创建;`app/accounts.py` 已实现账号 CRUD 服务、端口默认分配、启动登录、检测登录、生成快捷方式;`app/chrome.py` 已实现 Chrome 参数拼装、启动、CDP 端口探测、PowerShell `.lnk` 快捷方式生成;`app/gui.py` 已实现 PySide6 `MainWindow`、五 Tab、顶部 Tab 栏防误点样式、① 导入采集的 Excel 导入按钮/导入汇总栏/QTableView 任务列表/未匹配筛选与略过标记/采集旧标题旧封面 worker/采集前账号就绪预检与④引导/采集完成自动回写/旧数据回写重试按钮与 worker、② AI生成左右布局/标题与封面提示词管理/批次店铺状态筛选/任务列表/变量预览/开始生成/停止/进度/双击新旧封面预览与 `GenerateWorker`、③ 更新shopee筛选栏/任务列表/开始更新确认弹窗/确认后 `ApplyWorker` 串行更新/账号就绪预检与④引导/逐条 `set_applied`/自动回写结果到 Excel/结束汇总弹窗/手动回写重试按钮、④ 账号管理表格/弹窗/按钮/快捷方式与状态栏、⑤ 设置 AI 模型 master-detail 管理、`AIModelTestWorker` 后台测试连接、角色/生成参数/路径/端口配置并持久化 `config.json`;`app/workers.py` 已实现 `BaseWorker`、通用 signals、取消标记和 `QThread` 启动包装。
|
||||
- 测试:`tests/` 已建立;T-006 后纯逻辑改动必须运行 `python -m unittest discover -s tests`,当前覆盖 appconfig/db/config/accounts/chrome 启动与快捷方式/editor 登录检测与商品 tab 生命周期/excel 导入/旧字段与更新结果回写/ai 标题与封面 HTTP 解析/`generate_batch` 正常、失败与停止/prompts 读写与渲染/gui ① 导入采集/gui ② AI生成布局筛选、提示词管理、生成 worker 与双击预览/gui ③ 更新shopee筛选列表、确认弹窗、`ApplyWorker` 串行更新与账号预检、结果回写与汇总/gui ④ 账号管理/gui ⑤ AI 模型管理和角色/生成参数设置/worker signal 与线程包装;CDP/Shopee 改动仍需测试商品手动验证。
|
||||
- 数据:`config.json`、`config/ai_models.json`、`cmshopee.db`、`chrome_user_data_dir/`、`images/` 已由 `.gitignore` 排除;运营填写后的 Excel 业务文件默认忽略,标准空模板 `shopee待处理任务模板.xlsx` 可提交;`app/appconfig.py` 首次读取缺失的 `config.json` 时会在本地写默认配置,`app/db.py` 调用 `init_db()` 时会在本地创建 SQLite DB。
|
||||
|
||||
## 既定设计要点(文档已定)
|
||||
@@ -32,7 +32,7 @@
|
||||
| `prototypes/` | 已有 | 已验证原型/探查脚本(demo/set_title/set_cover/get_title/cookies/inspect_images/grab/1.py),保留作人工回归与探查参考;见 `prototypes/README.md` |
|
||||
| `chrome-remote-debug-lan.md` | 已有 | WSL→Windows CDP 转发排查记录 |
|
||||
| `app/__init__.py` / `app/__main__.py` / `main.py` | 已有 | 正式包与启动入口;`python main.py` / `python -m app` 可运行占位入口 |
|
||||
| `app/gui.py` | 已有 | T-104/T-105/T-106/T-202/T-202b/T-203/T-204/T-204b/T-205/T-302/T-302p/T-303/T-401/T-402/T-403/T-501 产出:PySide6 `QMainWindow` + 五 Tab;顶部 Tab 栏防误点样式;① 导入采集导入按钮、导入汇总栏、`QTableView` 任务列表、未匹配筛选与略过标记、采集旧标题旧封面 worker、采集前账号就绪预检与④引导、采集完成自动回写与手动重试;② AI生成左右布局、提示词管理、筛选栏、任务列表、开始生成/停止/进度、双击新旧封面预览与 `GenerateWorker`;③ 更新shopee筛选栏、任务列表、开始更新确认弹窗、确认后 `ApplyWorker` 串行更新、账号预检与④引导、逐条 `set_applied`、自动回写结果到 Excel、结束汇总弹窗与手动回写重试;④ 账号管理表格、账号弹窗、启动登录、检测登录、快捷方式;⑤ 设置 AI 模型下拉、新增/删除、详情编辑、密钥打码、测试连接 worker |
|
||||
| `app/gui.py` | 已有 | T-104/T-105/T-106/T-202/T-202b/T-203/T-204/T-204b/T-205/T-302/T-302p/T-303/T-401/T-402/T-403/T-501/T-501b 产出:PySide6 `QMainWindow` + 五 Tab;顶部 Tab 栏防误点样式;① 导入采集导入按钮、导入汇总栏、`QTableView` 任务列表、未匹配筛选与略过标记、采集旧标题旧封面 worker、采集前账号就绪预检与④引导、采集完成自动回写与手动重试;② AI生成左右布局、提示词管理、筛选栏、任务列表、开始生成/停止/进度、双击新旧封面预览与 `GenerateWorker`;③ 更新shopee筛选栏、任务列表、开始更新确认弹窗、确认后 `ApplyWorker` 串行更新、账号预检与④引导、逐条 `set_applied`、自动回写结果到 Excel、结束汇总弹窗与手动回写重试;④ 账号管理表格、账号弹窗、启动登录、检测登录、快捷方式;⑤ 设置 AI 模型下拉、新增/删除、详情编辑、密钥打码、测试连接 worker、默认角色下拉、生成参数、路径端口配置 |
|
||||
| `app/workers.py` | 已有 | T-104b 产出:`BaseWorker` + 通用 signals + 取消标记 + `run_worker()` QThread 包装 |
|
||||
| `app/accounts.py` | 已有 | T-105/T-106 产出:账号 CRUD 服务、目录创建、端口分配、启动登录、检测登录、快捷方式 |
|
||||
| `app/editor.py` | 已有 | T-001/T-103/T-205b 产出:登录状态检测、打开商品页、读/写标题、读/下载封面、采集后关闭自动新建商品页 tab、上传拖封面、更新按钮、apply_task |
|
||||
@@ -42,7 +42,7 @@
|
||||
| `app/db.py` | 已有 | T-003 产出:batches/accounts/tasks schema;WAL/busy_timeout/foreign_keys;账号/批次/任务与 set_* 阶段写库 |
|
||||
| `app/config.py` | 已有 | T-101 产出:别名→稳定 slug;创建并返回绝对 user-data-dir |
|
||||
| `app/chrome.py` | 已有 | T-102/T-106 产出:Chrome 启动参数、`subprocess.Popen` 启动、`/json/version` 端口探测、PowerShell `.lnk` 快捷方式 |
|
||||
| `tests/` | 已有 | T-006/T-201/T-202/T-202b/T-203/T-204/T-204b/T-205/T-301/T-302/T-302p/T-303/T-401/T-402/T-403/T-501 产出:stdlib unittest 基座;覆盖 appconfig/db/config/accounts/chrome/editor/excel/ai/prompts/gui/workers |
|
||||
| `tests/` | 已有 | T-006/T-201/T-202/T-202b/T-203/T-204/T-204b/T-205/T-301/T-302/T-302p/T-303/T-401/T-402/T-403/T-501/T-501b 产出:stdlib unittest 基座;覆盖 appconfig/db/config/accounts/chrome/editor/excel/ai/prompts/gui/workers |
|
||||
| `app/excel.py` | 已有 | T-201/T-204/T-403 产出:多文件 Excel 输入列解析、必需列整文件拒绝、脏行逐行跳过、批次/任务入库、匹配统计;按源文件/工作表/行号回写旧标题与旧封面路径;按源文件/工作表/行号回写新标题、新封面路径、更新状态;支持原文件被占用时另存副本 |
|
||||
| `shopee待处理任务模板.xlsx` | 已有,已提交 | 标准空 Excel 模板;单工作表 `待处理任务`,表头 `账号名 | 别名 | 商品id | 旧标题 | 旧封面图片路径 | 新标题 | 新封面图片路径 | 更新状态`;运营复制后填写,填写副本不提交 |
|
||||
| `config.json` / `config/ai_models.json` / `cmshopee.db` / `chrome_user_data_dir/` / `images/` | 本地存在或按需生成,已忽略 | 含配置、密钥、业务、登录态、图片,不提交版本库 |
|
||||
@@ -60,14 +60,14 @@
|
||||
|
||||
任务状态以 [`06-tasks.md`](06-tasks.md) 为准,历史记录见 [`../progress.md`](../progress.md)。
|
||||
|
||||
- 已完成:T-000(正式代码包结构)、T-001(`app/editor.py` 模块化)、T-002(`app/appconfig.py` + `config.json`)、T-003(SQLite 建表)、T-004(本地数据 gitignore)、T-005(AI 模型清单后端)、T-006(单元测试基座)、T-101(账号 slug/user-data-dir)、T-102(Chrome 启动器)、T-103(登录保活与检测)、T-104(PySide6 五 Tab 主窗口骨架)、T-104b(PySide6 worker 基座)、T-105(Tab④ 账号管理)、T-106(账号快捷方式)、T-201(Excel 导入:解析多文件输入列入库)、T-202(Tab① 任务列表 + 导入按钮 + 别名匹配标记)、T-202b(Tab① 导入汇总栏)、T-203(采集旧标题+旧封面)、T-204(回写旧字段到原 Excel)、T-204b(采集完成自动回写旧字段)、T-205(首次未配账号 / Chrome 未启动 / 未登录引导保护)、T-205b(采集后关闭自动新建商品页 tab)、T-301(AI 生成接口)、T-302(Tab② 左右布局与任务列表)、T-302p(提示词管理)、T-303(Tab② 开始生成 + 停止 + 进度)、T-401(Tab③ 更新列表筛选 + 开始更新确认弹窗)、T-402(Tab③ 确认后串行更新)、T-403(Tab③ 结果回写与结束汇总)、T-501(Tab⑤ AI 模型管理 UI)。
|
||||
- 已完成:T-000(正式代码包结构)、T-001(`app/editor.py` 模块化)、T-002(`app/appconfig.py` + `config.json`)、T-003(SQLite 建表)、T-004(本地数据 gitignore)、T-005(AI 模型清单后端)、T-006(单元测试基座)、T-101(账号 slug/user-data-dir)、T-102(Chrome 启动器)、T-103(登录保活与检测)、T-104(PySide6 五 Tab 主窗口骨架)、T-104b(PySide6 worker 基座)、T-105(Tab④ 账号管理)、T-106(账号快捷方式)、T-201(Excel 导入:解析多文件输入列入库)、T-202(Tab① 任务列表 + 导入按钮 + 别名匹配标记)、T-202b(Tab① 导入汇总栏)、T-203(采集旧标题+旧封面)、T-204(回写旧字段到原 Excel)、T-204b(采集完成自动回写旧字段)、T-205(首次未配账号 / Chrome 未启动 / 未登录引导保护)、T-205b(采集后关闭自动新建商品页 tab)、T-301(AI 生成接口)、T-302(Tab② 左右布局与任务列表)、T-302p(提示词管理)、T-303(Tab② 开始生成 + 停止 + 进度)、T-401(Tab③ 更新列表筛选 + 开始更新确认弹窗)、T-402(Tab③ 确认后串行更新)、T-403(Tab③ 结果回写与结束汇总)、T-501(Tab⑤ AI 模型管理 UI)、T-501b(Tab⑤ 角色与生成参数)。
|
||||
- 正在进行:无。
|
||||
- 下一个可领取任务:**T-501b(Tab⑤ 设置 · 角色与生成参数)**。T-404 真实 Shopee 单条更新冒烟已登记,但依赖 T-501c 设置安全开关完成后再执行。
|
||||
- 下一个可领取任务:**T-501c(Tab⑤ 设置 · Shopee 更新安全开关)**。T-404 真实 Shopee 单条更新冒烟已登记,但依赖 T-501c 设置安全开关完成后再执行。
|
||||
|
||||
## 当前已知限制
|
||||
|
||||
- ① 采集依赖对应账号 Chrome 已用专属 user-data-dir 和 CDP 端口启动并登录;T-205 已在采集前拦截未配置账号、Chrome 未启动、未登录,并引导去④账号管理,但不会无提示批量启动所有账号 Chrome。
|
||||
- T-501 已完成 `config/ai_models.json` 模型清单 UI;标题/图片默认模型角色、并发、重试、分辨率、jpg 质量、路径/端口等仍留给 T-501b。
|
||||
- T-501/T-501b 已完成 `config/ai_models.json` 模型清单 UI,以及 `config.json` 里的标题/图片默认模型角色、并发、重试、分辨率、jpg 质量、路径/端口设置。
|
||||
- T-301/T-303 已完成通用 HTTP AI 接口、批量生成编排和 GUI 接入 mock 单测;真实 AI 生成还需要在 `config/ai_models.json` 填入可用 url/model/api_key 后做一次成本可控的小样本实测。
|
||||
- T-403 已完成③更新结果回写与结束汇总;真实 Shopee 更新冒烟暂不执行,延后到 T-501c 设置安全开关完成后做 T-404 单条验收。T-404 默认先只测标题更新,封面更新作为可选子项。
|
||||
|
||||
|
||||
+6
-3
@@ -128,8 +128,11 @@
|
||||
- 密钥存在 `config/ai_models.json`,本地明文保存;UI 使用密码框打码显示,不进入日志。
|
||||
- 保存复用 `appconfig` 模型清单后端,保持 name 唯一、至少 text/image 各一个模型。
|
||||
- 「测试连接」通过后台 worker 调 `appconfig.test_ai_model()`,不阻塞主线程。
|
||||
- AI 生成参数(T-501b):标题/图片模型角色下拉、标题并发数、图片并发数、失败重试次数、分辨率、jpg 质量。
|
||||
- 路径与端口(T-501b):本地图片目录、Chrome 路径、默认调试端口 / 端口范围、超时、DB 路径。
|
||||
- AI 生成参数(T-501b 已接入):标题/图片模型角色下拉、标题并发数、图片并发数、失败重试次数、分辨率、jpg 质量。
|
||||
- 标题模型下拉只列 text 类模型;图片模型下拉只列 image 类模型。
|
||||
- 分辨率为 `512 / 1k / 2k / 4k`;返回超时只读展示 `resolution_timeouts[resolution]`,不单独编辑。
|
||||
- 保存写入 `config.json` 的 `ai` 段,供 ② AI生成复用。
|
||||
- 路径与端口(T-501b 已接入):本地图片目录、Chrome 路径、账号数据根目录、DB 路径、默认调试端口、端口范围、CDP 就绪超时。
|
||||
|
||||
## 流程导航
|
||||
|
||||
@@ -156,7 +159,7 @@
|
||||
| `GenerateTab(QWidget)` | ② | 左提示词管理 + 右筛选/任务列表;双击看新旧封面;开始生成/停止/进度已接入 `GenerateWorker` |
|
||||
| `ApplyTab(QWidget)` | ③ | 已生成任务筛选 + 开始更新确认 + 确认后串行更新 + 结果回写与结束汇总 |
|
||||
| `AccountsTab(QWidget)` | ④ | 账号增删改、启动登录、检测登录、生成快捷方式 |
|
||||
| `SettingsTab(QWidget)` | ⑤ | AI 模型 master-detail 管理;后续接入生成参数、目录、Chrome 配置 |
|
||||
| `SettingsTab(QWidget)` | ⑤ | AI 模型 master-detail 管理 + 角色/生成参数/路径/端口配置 |
|
||||
| `TaskTableModel(QAbstractTableModel)` | ①②③ | 任务表格数据模型,供 `QTableView` 使用 |
|
||||
| `BaseWorker(QObject)` | 后台 | 定义 `progress/log/row_updated/failed/finished/cancelled` signals |
|
||||
| `ApplyWorker(BaseWorker)` | ③ | 账号就绪预检、串行调用 `editor.apply_task()`、逐条 `set_applied()`,失败继续 |
|
||||
|
||||
+10
@@ -585,3 +585,13 @@
|
||||
- 测试:`tests/test_gui.py` 覆盖 MainWindow 挂载⑤、模型加载与密钥打码、新增/保存/删除、测试连接 worker、`AIModelTestWorker.execute()`。
|
||||
- 文档:`docs/06-tasks.md` 将 T-501 标为 DONE;同步 `docs/routes.md`、`docs/api.md`、`docs/current-state.md`,下一个可领取任务更新为 T-501b。
|
||||
- 验证:`python -m compileall app main.py tests` 通过;`python -m unittest discover -s tests -p test_gui.py` 通过(36 tests);`python -m unittest discover -s tests` 通过(92 tests)。
|
||||
|
||||
## 【2026-06-29】T-501b Tab⑤ 角色与生成参数
|
||||
|
||||
- 状态:DONE
|
||||
- 变更:`app/gui.py` 扩展 `SettingsTab`,⑤设置页新增角色与生成参数、路径与端口配置区。标题大模型下拉只列 text 类模型,图片大模型下拉只列 image 类模型;支持标题/图片并发、失败重试、分辨率、jpg 质量、Chrome 路径、账号数据根目录、图片目录、DB 路径、默认调试端口、端口范围、CDP 就绪超时。
|
||||
- 配置:点击「保存设置」写入 `config.json`;保留 `resolution_timeouts`,分辨率切换时只读展示对应返回超时;保存前校验端口范围和默认端口。
|
||||
- 细节:`SettingsTab` 和 `MainWindow` 支持注入 `config_path` 便于测试;保存时不会把测试用的 `config_path`/`ai_models_path` 写入 `config.json`。
|
||||
- 测试:`tests/test_gui.py` 覆盖角色下拉类别过滤、分辨率返回超时显示、生成参数/路径/端口保存到临时 `config.json`、非法端口范围拦截。
|
||||
- 文档:`docs/06-tasks.md` 将 T-501b 标为 DONE;同步 `docs/routes.md`、`docs/api.md`、`docs/current-state.md`,下一个可领取任务更新为 T-501c。
|
||||
- 验证:`python -m compileall app main.py tests` 通过;`python -m unittest discover -s tests -p test_gui.py` 通过(38 tests);`python -m unittest discover -s tests` 通过(94 tests)。
|
||||
|
||||
@@ -54,6 +54,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
"db_path": os.path.join(temp_dir, "cmshopee.db"),
|
||||
"debug_port_range": [9222, 9260],
|
||||
"ai_models_path": os.path.join(temp_dir, "ai_models.json"),
|
||||
"config_path": os.path.join(temp_dir, "config.json"),
|
||||
}
|
||||
|
||||
def test_main_window_has_five_tabs_in_workflow_order(self):
|
||||
@@ -215,6 +216,153 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_settings_tab_saves_role_generation_path_and_port_config(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
cfg["ai"] = appconfig.default_config()["ai"]
|
||||
cfg["ai"]["default_text_model"] = "Text A"
|
||||
cfg["ai"]["default_image_model"] = "Image A"
|
||||
models_path = cfg["ai_models_path"]
|
||||
config_path = cfg["config_path"]
|
||||
appconfig.save_ai_models_config(
|
||||
{
|
||||
"models": [
|
||||
{
|
||||
"name": "Text A",
|
||||
"category": "text",
|
||||
"enabled": True,
|
||||
"url": "",
|
||||
"model": "",
|
||||
"api_key": "",
|
||||
"api_type": "chat",
|
||||
"connect_timeout_seconds": 30,
|
||||
"timeout_seconds": 0,
|
||||
"extra_body": {},
|
||||
},
|
||||
{
|
||||
"name": "Text B",
|
||||
"category": "text",
|
||||
"enabled": True,
|
||||
"url": "",
|
||||
"model": "",
|
||||
"api_key": "",
|
||||
"api_type": "chat",
|
||||
"connect_timeout_seconds": 30,
|
||||
"timeout_seconds": 0,
|
||||
"extra_body": {},
|
||||
},
|
||||
{
|
||||
"name": "Image A",
|
||||
"category": "image",
|
||||
"enabled": True,
|
||||
"url": "",
|
||||
"model": "",
|
||||
"api_key": "",
|
||||
"api_type": "auto",
|
||||
"connect_timeout_seconds": 30,
|
||||
"timeout_seconds": 0,
|
||||
"extra_body": {},
|
||||
},
|
||||
{
|
||||
"name": "Image B",
|
||||
"category": "image",
|
||||
"enabled": True,
|
||||
"url": "",
|
||||
"model": "",
|
||||
"api_key": "",
|
||||
"api_type": "auto",
|
||||
"connect_timeout_seconds": 30,
|
||||
"timeout_seconds": 0,
|
||||
"extra_body": {},
|
||||
},
|
||||
]
|
||||
},
|
||||
path=models_path,
|
||||
)
|
||||
statuses = []
|
||||
tab = SettingsTab(
|
||||
config=cfg,
|
||||
config_path=config_path,
|
||||
ai_models_path=models_path,
|
||||
status_callback=statuses.append,
|
||||
)
|
||||
self.addCleanup(tab.close)
|
||||
|
||||
text_roles = [
|
||||
tab.default_text_model_combo.itemData(index)
|
||||
for index in range(tab.default_text_model_combo.count())
|
||||
]
|
||||
image_roles = [
|
||||
tab.default_image_model_combo.itemData(index)
|
||||
for index in range(tab.default_image_model_combo.count())
|
||||
]
|
||||
self.assertEqual(["Text A", "Text B"], text_roles)
|
||||
self.assertEqual(["Image A", "Image B"], image_roles)
|
||||
|
||||
tab.default_text_model_combo.setCurrentIndex(
|
||||
tab.default_text_model_combo.findData("Text B")
|
||||
)
|
||||
tab.default_image_model_combo.setCurrentIndex(
|
||||
tab.default_image_model_combo.findData("Image B")
|
||||
)
|
||||
tab.title_concurrency_spin.setValue(3)
|
||||
tab.image_concurrency_spin.setValue(2)
|
||||
tab.retry_spin.setValue(1)
|
||||
tab.resolution_combo.setCurrentIndex(tab.resolution_combo.findData("2k"))
|
||||
self.assertEqual("360 秒", tab.response_timeout_label.text())
|
||||
tab.jpg_quality_spin.setValue(86)
|
||||
tab.chrome_path_edit.setText("D:\\Chrome\\chrome.exe")
|
||||
tab.user_data_root_edit.setText("profiles")
|
||||
tab.image_dir_edit.setText("pictures")
|
||||
tab.db_path_edit.setText("data\\cmshopee.db")
|
||||
tab.default_debug_port_spin.setValue(9300)
|
||||
tab.debug_port_start_spin.setValue(9300)
|
||||
tab.debug_port_end_spin.setValue(9350)
|
||||
tab.cdp_ready_timeout_spin.setValue(45)
|
||||
|
||||
tab.save_app_settings()
|
||||
|
||||
saved = appconfig.load_config(config_path)
|
||||
self.assertEqual("Text B", saved["ai"]["default_text_model"])
|
||||
self.assertEqual("Image B", saved["ai"]["default_image_model"])
|
||||
self.assertEqual(3, saved["ai"]["title_concurrency"])
|
||||
self.assertEqual(2, saved["ai"]["image_concurrency"])
|
||||
self.assertEqual(1, saved["ai"]["retry"])
|
||||
self.assertEqual("2k", saved["ai"]["resolution"])
|
||||
self.assertEqual(86, saved["ai"]["jpg_quality"])
|
||||
self.assertEqual("D:\\Chrome\\chrome.exe", saved["chrome_path"])
|
||||
self.assertEqual("profiles", saved["user_data_root"])
|
||||
self.assertEqual("pictures", saved["image_dir"])
|
||||
self.assertEqual("data\\cmshopee.db", saved["db_path"])
|
||||
self.assertEqual(9300, saved["default_debug_port"])
|
||||
self.assertEqual([9300, 9350], saved["debug_port_range"])
|
||||
self.assertEqual(45, saved["cdp_ready_timeout"])
|
||||
self.assertNotIn("ai_models_path", saved)
|
||||
self.assertNotIn("config_path", saved)
|
||||
self.assertIn("设置已保存", statuses[-1])
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_settings_tab_rejects_invalid_port_range(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
tab = SettingsTab(
|
||||
config=cfg,
|
||||
config_path=cfg["config_path"],
|
||||
ai_models_path=cfg["ai_models_path"],
|
||||
)
|
||||
self.addCleanup(tab.close)
|
||||
|
||||
tab.debug_port_start_spin.setValue(9400)
|
||||
tab.debug_port_end_spin.setValue(9300)
|
||||
with mock.patch("app.gui.QMessageBox.warning") as warning:
|
||||
tab.save_app_settings()
|
||||
|
||||
warning.assert_called_once()
|
||||
self.assertFalse(os.path.exists(cfg["config_path"]))
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_ai_model_test_worker_calls_appconfig(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
models_path = os.path.join(temp_dir, "ai_models.json")
|
||||
|
||||
Reference in New Issue
Block a user