diff --git a/app/gui.py b/app/gui.py index af47a31..8c609f4 100644 --- a/app/gui.py +++ b/app/gui.py @@ -19,6 +19,7 @@ try: QDialogButtonBox, QFileDialog, QFormLayout, + QGridLayout, QHBoxLayout, QHeaderView, QInputDialog, @@ -2857,18 +2858,6 @@ if QT_IMPORT_ERROR is None: self.add_model_button = QPushButton("新增") self.delete_model_button = QPushButton("删除") - left_panel = QWidget() - left_layout = QVBoxLayout(left_panel) - left_layout.setContentsMargins(0, 0, 12, 0) - left_layout.addWidget(QLabel("AI 模型")) - left_layout.addWidget(self.model_combo) - left_toolbar = QHBoxLayout() - left_toolbar.addWidget(self.add_model_button) - left_toolbar.addWidget(self.delete_model_button) - left_toolbar.addStretch(1) - left_layout.addLayout(left_toolbar) - left_layout.addStretch(1) - self.enabled_checkbox = QCheckBox("启用") self.name_edit = QLineEdit() self.name_edit.setObjectName("modelNameEdit") @@ -2956,88 +2945,104 @@ if QT_IMPORT_ERROR is None: self.max_parallel_accounts_spin.setObjectName("maxParallelAccountsSpin") self.max_parallel_accounts_spin.setRange(1, 16) - form = QFormLayout() - form.addRow("", self.enabled_checkbox) - form.addRow("服务商名", self.name_edit) - form.addRow("类别", self.category_combo) - form.addRow("api_type", self.api_type_combo) - form.addRow("模型ID", self.model_id_edit) - form.addRow("网址", self.url_edit) - form.addRow("密钥", self.api_key_edit) - form.addRow("连接超时(秒)", self.connect_timeout_spin) + model_picker_layout = QHBoxLayout() + model_picker_layout.addWidget(self.model_combo, 1) + model_picker_layout.addWidget(self.add_model_button) + model_picker_layout.addWidget(self.delete_model_button) action_layout = QHBoxLayout() action_layout.addWidget(self.save_model_button) 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) + form = self._two_column_form( + [ + ("状态", self.enabled_checkbox), + ("服务商名", self.name_edit), + ("类别", self.category_combo), + ("api_type", self.api_type_combo), + ("模型ID", self.model_id_edit), + ("连接超时(秒)", self.connect_timeout_spin), + ("网址", self.url_edit, True), + ("密钥", self.api_key_edit, True), + ] + ) + + ai_form = self._two_column_form( + [ + ("标题大模型", self.default_text_model_combo), + ("图片大模型", self.default_image_model_combo), + ("标题并发数", self.title_concurrency_spin), + ("图片并发数", self.image_concurrency_spin), + ("失败重试次数", self.retry_spin), + ("分辨率", self.resolution_combo), + ("返回超时", self.response_timeout_label), + ("jpg质量", self.jpg_quality_spin), + ] + ) port_range_layout = QHBoxLayout() + port_range_layout.setContentsMargins(0, 0, 0, 0) port_range_layout.addWidget(self.debug_port_start_spin) port_range_layout.addWidget(QLabel("到")) port_range_layout.addWidget(self.debug_port_end_spin) + port_range_widget = QWidget() + port_range_widget.setLayout(port_range_layout) - 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) + path_form = self._two_column_form( + [ + ("Chrome路径", self.chrome_path_edit, True), + ("账号数据根目录", self.user_data_root_edit), + ("图片目录", self.image_dir_edit), + ("DB路径", self.db_path_edit), + ("默认调试端口", self.default_debug_port_spin), + ("调试端口范围", port_range_widget), + ("CDP就绪超时(秒)", self.cdp_ready_timeout_spin), + ] + ) - update_form = QFormLayout() - update_form.addRow("测试商品ID", self.test_item_id_edit) - update_form.addRow("", self.allow_real_submit_checkbox) - update_form.addRow("", self.allow_cover_update_checkbox) - update_form.addRow("单次最大更新条数", self.max_items_per_run_spin) - update_form.addRow("", self.close_success_tab_checkbox) - update_form.addRow("", self.dry_run_checkbox) - update_form.addRow("", self.parallel_accounts_checkbox) - update_form.addRow("最大并行账号数", self.max_parallel_accounts_spin) + update_form = self._two_column_form( + [ + ("测试商品ID", self.test_item_id_edit), + ("单次最大更新条数", self.max_items_per_run_spin), + ("", self.allow_real_submit_checkbox), + ("", self.allow_cover_update_checkbox), + ("", self.close_success_tab_checkbox), + ("", self.dry_run_checkbox), + ("", self.parallel_accounts_checkbox), + ("最大并行账号数", self.max_parallel_accounts_spin), + ] + ) - right_panel = QWidget() - right_layout = QVBoxLayout(right_panel) - right_layout.setContentsMargins(12, 0, 0, 0) - right_layout.addWidget(QLabel("模型详情")) - 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.addSpacing(18) - right_layout.addWidget(QLabel("Shopee 更新安全")) - right_layout.addLayout(update_form) - right_layout.addWidget(self.save_config_button) - right_layout.addStretch(1) + panel = QWidget() + panel_layout = QVBoxLayout(panel) + panel_layout.setContentsMargins(0, 0, 0, 0) + panel_layout.addWidget(QLabel("AI 模型")) + panel_layout.addLayout(model_picker_layout) + panel_layout.addSpacing(14) + panel_layout.addWidget(QLabel("模型详情")) + panel_layout.addLayout(form) + panel_layout.addLayout(action_layout) + panel_layout.addWidget(self.test_result_label) + panel_layout.addSpacing(18) + panel_layout.addWidget(QLabel("角色与生成参数")) + panel_layout.addLayout(ai_form) + panel_layout.addSpacing(18) + panel_layout.addWidget(QLabel("路径与端口")) + panel_layout.addLayout(path_form) + panel_layout.addSpacing(18) + panel_layout.addWidget(QLabel("Shopee 更新安全")) + panel_layout.addLayout(update_form) + panel_layout.addWidget(self.save_config_button) + panel_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_scroll) - self.splitter.setStretchFactor(0, 1) - self.splitter.setStretchFactor(1, 3) - self.splitter.setSizes([280, 860]) + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll.setWidget(panel) layout = QVBoxLayout(self) layout.setContentsMargins(18, 18, 18, 18) - layout.addWidget(self.splitter, 1) + layout.addWidget(scroll, 1) self.model_combo.currentIndexChanged.connect(self.load_selected_model) self.add_model_button.clicked.connect(self.add_model) @@ -3052,6 +3057,47 @@ if QT_IMPORT_ERROR is None: self.refresh_models() self._populate_app_settings() + def _two_column_form(self, fields): + layout = QGridLayout() + layout.setHorizontalSpacing(18) + layout.setVerticalSpacing(8) + layout.setColumnStretch(1, 1) + layout.setColumnStretch(3, 1) + row = 0 + column_pair = 0 + for field in fields: + label = field[0] + widget = field[1] + full_width = len(field) > 2 and bool(field[2]) + if full_width and column_pair: + row += 1 + column_pair = 0 + column = column_pair * 2 + self._add_form_field(layout, row, column, label, widget, full_width) + if full_width: + row += 1 + column_pair = 0 + elif column_pair == 0: + column_pair = 1 + else: + row += 1 + column_pair = 0 + return layout + + def _add_form_field(self, layout, row, column, label, widget, full_width): + if full_width: + if label: + layout.addWidget(QLabel(label), row, 0) + layout.addWidget(widget, row, 1, 1, 3) + else: + layout.addWidget(widget, row, 0, 1, 4) + return + if label: + layout.addWidget(QLabel(label), row, column) + layout.addWidget(widget, row, column + 1) + else: + layout.addWidget(widget, row, column, 1, 2) + def _set_status(self, message): if self.status_callback is not None: self.status_callback(message) diff --git a/docs/api.md b/docs/api.md index b0eb820..1a73f37 100644 --- a/docs/api.md +++ b/docs/api.md @@ -344,7 +344,7 @@ TAB_STYLE: str # 顶层 Tab 栏防误点样式: ⑤ 设置当前要点(T-501): -- `SettingsTab` 左侧为 AI 模型下拉与新增/删除按钮,右侧为模型详情表单。 +- `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`。 - 密钥字段使用 `QLineEdit.Password` 打码显示;首次保存/变更非空 Key 前弹窗提示“本地明文保存”;明文只写入已 gitignore 的 `config/ai_models.json`,不得进入日志/导出。 diff --git a/docs/current-state.md b/docs/current-state.md index dd6f286..1da4c77 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -8,7 +8,7 @@ - 日期: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⑤ 角色与生成参数、T-501c Tab⑤ Shopee 更新安全开关、T-502 满 9 张封面删除再上传、T-503 敏感信息本地明文保存提示与日志脱敏、T-504 多账号并行/dry-run/运行日志。 - 技术栈: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,更新提交成功且设置开启时可关闭本轮自动新建商品页;`replace_cover()` 已实现满 9 张时先校验本地旧封面备份,再点第一张删除、可见确认框、上传新图并拖到第一位的代码路径;`app/appconfig.py` 已实现 `config.json` 默认值/读写/更新、AI 参数、端口读取、Shopee 更新安全与执行模式默认值,`config/ai_models.json` 模型清单 CRUD/过滤/打码/测试连接,以及 `mask_secret()`、`sanitize_for_log()`、`redact_secrets()` 敏感信息脱敏工具;`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、批次/账号/任务与阶段写库函数,以及 `run_logs/run_log_events` 运行日志函数;`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` dry-run 预览或真实更新/账号就绪和端口冲突预检/按账号并行可选/逐条 `set_applied`/运行日志/自动回写结果到 Excel/结束汇总弹窗/手动回写重试按钮、④ 账号管理表格/弹窗/按钮/快捷方式与状态栏、密码明文保存提示、⑤ 设置 AI 模型 master-detail 管理、API Key 明文保存提示、`AIModelTestWorker` 后台测试连接、角色/生成参数/路径/端口配置、Shopee 更新安全和 dry-run/多账号并行设置并持久化 `config.json`;`app/workers.py` 已实现 `BaseWorker`、通用 signals、取消标记和 `QThread` 启动包装。 +- 生产代码:已建立 `app/` 包 + 根入口 `main.py`;`app/cdp.py` 为已验证 CDP 底座,已区分 `CDP.close()` 断开 WebSocket 与 `close_tab()` 关闭浏览器 target;`app/editor.py` 已封装登录状态检测、标题/封面/采集/更新按钮能力,并在采集结束后只关闭本轮自动新建的商品编辑页 tab、保留用户已有 tab,更新提交成功且设置开启时可关闭本轮自动新建商品页;`replace_cover()` 已实现满 9 张时先校验本地旧封面备份,再点第一张删除、可见确认框、上传新图并拖到第一位的代码路径;`app/appconfig.py` 已实现 `config.json` 默认值/读写/更新、AI 参数、端口读取、Shopee 更新安全与执行模式默认值,`config/ai_models.json` 模型清单 CRUD/过滤/打码/测试连接,以及 `mask_secret()`、`sanitize_for_log()`、`redact_secrets()` 敏感信息脱敏工具;`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、批次/账号/任务与阶段写库函数,以及 `run_logs/run_log_events` 运行日志函数;`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` dry-run 预览或真实更新/账号就绪和端口冲突预检/按账号并行可选/逐条 `set_applied`/运行日志/自动回写结果到 Excel/结束汇总弹窗/手动回写重试按钮、④ 账号管理表格/弹窗/按钮/快捷方式与状态栏、密码明文保存提示、⑤ 设置 AI 模型同列上下布局、两列详情/参数表单、API Key 明文保存提示、`AIModelTestWorker` 后台测试连接、角色/生成参数/路径/端口配置、Shopee 更新安全和 dry-run/多账号并行设置并持久化 `config.json`;`app/workers.py` 已实现 `BaseWorker`、通用 signals、取消标记和 `QThread` 启动包装。 - 测试:`tests/` 已建立;T-006 后纯逻辑改动必须运行 `python -m unittest discover -s tests`,当前覆盖 appconfig/db/config/accounts/chrome 启动与快捷方式/editor 登录检测与商品 tab 生命周期、更新成功后关闭本轮新开 tab、满 9 张封面备份缺失阻断/删除确认/上传拖首位 mock 路径/excel 导入/旧字段与更新结果回写/ai 标题与封面 HTTP 解析/`generate_batch` 正常、失败与停止/prompts 读写与渲染/gui ① 导入采集/gui ② AI生成布局筛选、提示词管理、生成 worker 与双击预览/gui ③ 更新shopee筛选列表、确认弹窗、Shopee 更新安全拦截、`ApplyWorker` 串行/dry-run/按账号并行/端口冲突预检、运行日志、结果回写与汇总/gui ④ 账号管理、密码打码与明文保存提示/gui ⑤ AI 模型管理、API Key 打码与明文保存提示、角色/生成参数设置、Shopee 更新安全设置、dry-run/多账号并行设置/worker signal 与线程包装;CDP/Shopee 改动仍需测试商品手动验证。 - 数据:`config.json`、`config/ai_models.json`、`cmshopee.db`、`chrome_user_data_dir/`、`images/` 已由 `.gitignore` 排除;密码与 API Key 本地明文保存但保存/变更时提示,UI 打码,日志/导出必须脱敏;运营填写后的 Excel 业务文件默认忽略,标准空模板 `shopee待处理任务模板.xlsx` 可提交;`app/appconfig.py` 首次读取缺失的 `config.json` 时会在本地写默认配置,`app/db.py` 调用 `init_db()` 时会在本地创建 SQLite DB。 diff --git a/docs/routes.md b/docs/routes.md index 4dfa711..2ba8b85 100644 --- a/docs/routes.md +++ b/docs/routes.md @@ -129,7 +129,7 @@ ## ⑤ 设置 -- AI 模型管理(T-501 已接入):左侧模型下拉 + 新增/删除,右侧详情编辑。 +- AI 模型管理(T-501 已接入):同列上下布局,AI 模型下拉 + 新增/删除在“模型详情”上方;详情与设置表单尽量两项一行,长字段独占整行。 - 字段:启用、服务商名、类别(文本/图像)、api_type、模型ID、网址、密钥、连接超时。 - 密钥存在 `config/ai_models.json`,本地明文保存;保存/变更时提示;UI 使用密码框打码显示,不进入日志/导出。 - 保存复用 `appconfig` 模型清单后端,保持 name 唯一、至少 text/image 各一个模型。 diff --git a/docs/ui/tab5-settings.svg b/docs/ui/tab5-settings.svg index 921ccb7..c130426 100644 --- a/docs/ui/tab5-settings.svg +++ b/docs/ui/tab5-settings.svg @@ -18,31 +18,29 @@ ③ 更新shopee④ 账号管理 ⑤ 设置 - + AI 模型管理 - - - 模型 - Nano Banana 2 ▼ - 新增 - 删除 - 测试连接 - 至少各 1 个 文本/图像模型 + + AI 模型 + Nano Banana 2 · 图像 ▼ + 新增 + 删除 + 至少各 1 个 文本/图像模型 - - 服务商名称Nano Banana 2 - 网址https://api.vectorengine.ai/v1/chat/completions + + 模型详情 + 服务商名Nano Banana 2 + 类别图像 ▼ + api_typeauto ▼ + 连接超时30秒 - 类别图像 ▼ - api_typeauto ▼ - 连接超时30秒 - - 模型IDgemini-3.1-flash-image-preview - 密钥•••••••• - 👁 + 网址https://api.vectorengine.ai/v1/chat/completions + 密钥•••••••• + 保存 + 测试连接 diff --git a/progress.md b/progress.md index ccb5046..5782bc3 100644 --- a/progress.md +++ b/progress.md @@ -660,3 +660,10 @@ - 原因:本地 gitignored 的 `config/ai_models.json` 来自旧数据或手工维护,模型缺少 `category`,而当前 schema 要求每个模型必须是 `text` 或 `image`。 - 处理:已在本机只补齐模型 `category` 和空的 `enabled=true`,保留原有 url/model/API Key;验证 `appconfig.list_ai_models()` 可正常加载,且 `config/ai_models.json` 仍被 gitignore。 - 文档:新增 `docs/troubleshooting.md`,并在 `docs/README.md`、`docs/03-tech-stack.md`、`docs/current-state.md` 增加索引和说明。排查命令只输出 `name/category/api_type/enabled/has_api_key`,不输出 API Key。 + +## 【2026-06-29】设置页 AI 模型布局微调 + +- 状态:DONE +- 变更:`app/gui.py` 将⑤设置页的 AI 模型下拉/新增/删除从左侧分栏移到“模型详情”上方,设置页改为单列滚动布局;模型详情、角色与生成参数、路径与端口、Shopee 更新安全表单尽量两项一行,URL/API Key/Chrome 路径等长字段独占整行。 +- 文档:同步 `docs/routes.md`、`docs/api.md`、`docs/current-state.md` 中的设置页布局描述,并更新 `docs/ui/tab5-settings.svg` UI 草图。 +- 验证:`python -m compileall app main.py tests` 通过;`python -m unittest discover -s tests -p test_gui.py` 通过(48 tests)。