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)。