From 742193607bcd024cb08cef55631228e867661241 Mon Sep 17 00:00:00 2001 From: chengma Date: Sat, 27 Jun 2026 10:38:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E6=A1=8C=E9=9D=A2=E5=BF=AB=E6=8D=B7=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 chrome.create_shortcut 使用 PowerShell WScript.Shell 生成 .lnk - 快捷方式参数复用 Chrome 启动参数,包含调试端口、allow-origins 和账号 user-data-dir - 在账号服务层和 Tab④ 增加快捷方式入口,不包含密码或密钥 - 补充 chrome/accounts/gui 测试并同步任务、API、路由、当前状态和进度文档 --- app/accounts.py | 10 ++++++ app/chrome.py | 76 ++++++++++++++++++++++++++++++++++++++++++ app/gui.py | 16 +++++++++ docs/06-tasks.md | 2 +- docs/api.md | 10 ++++-- docs/current-state.md | 16 ++++----- docs/routes.md | 2 +- progress.md | 8 +++++ tests/test_accounts.py | 26 +++++++++++++++ tests/test_chrome.py | 32 ++++++++++++++++++ tests/test_gui.py | 23 +++++++++++++ 11 files changed, 208 insertions(+), 13 deletions(-) diff --git a/app/accounts.py b/app/accounts.py index 4ab6d9b..1d196ed 100644 --- a/app/accounts.py +++ b/app/accounts.py @@ -213,6 +213,16 @@ def launch_for_login(account_or_alias, path=None, config=None): return chrome.launch_chrome(account, config=config) +def create_shortcut(account_or_alias, shortcut_path=None, desktop_dir=None, path=None, config=None) -> str: + account = resolve_account(account_or_alias, path=path, config=config) + return chrome.create_shortcut( + account, + shortcut_path=shortcut_path, + desktop_dir=desktop_dir, + config=config, + ) + + def detect_login(account_or_alias, timeout=8, path=None, config=None) -> dict: database_path = _db_path(path, config) account = resolve_account(account_or_alias, path=database_path, config=config) diff --git a/app/chrome.py b/app/chrome.py index 12983fa..e7800bd 100644 --- a/app/chrome.py +++ b/app/chrome.py @@ -4,6 +4,7 @@ from __future__ import annotations import json import os +import re import subprocess import time import urllib.error @@ -69,6 +70,81 @@ def build_launch_args(account, config=None) -> list: ] +def _safe_shortcut_name(value) -> str: + text = str(value or "").strip() + if not text: + raise ChromeLaunchError("快捷方式名称不能为空") + text = re.sub(r'[<>:"/\\|?*\x00-\x1f]+', "_", text) + text = re.sub(r"\s+", " ", text).strip(" .") + if not text: + raise ChromeLaunchError("快捷方式名称不能为空") + return text + + +def _default_shortcut_name(account) -> str: + alias = _account_value(account, "alias") + account_name = _account_value(account, "account_name") + name = alias or account_name or _required_account_value(account, "slug") + return _safe_shortcut_name(f"cmshopee-{name}") + + +def _desktop_dir() -> str: + return os.path.join(os.path.expanduser("~"), "Desktop") + + +def _ps_single_quote(value) -> str: + return "'" + str(value).replace("'", "''") + "'" + + +def create_shortcut(account, shortcut_path=None, desktop_dir=None, name=None, config=None) -> str: + """Create a Windows .lnk shortcut for one account's Chrome session.""" + + args = build_launch_args(account, config=config) + chrome_path = os.path.abspath(args[0]) + arguments = subprocess.list2cmdline(args[1:]) + if shortcut_path is None: + root = os.path.abspath(desktop_dir or _desktop_dir()) + shortcut_name = _safe_shortcut_name(name) if name else _default_shortcut_name(account) + shortcut_path = os.path.join(root, shortcut_name + ".lnk") + shortcut_path = os.path.abspath(str(shortcut_path)) + if not shortcut_path.lower().endswith(".lnk"): + shortcut_path += ".lnk" + os.makedirs(os.path.dirname(shortcut_path), exist_ok=True) + + working_directory = os.path.dirname(chrome_path) or os.getcwd() + script = "; ".join( + [ + "$shell = New-Object -ComObject WScript.Shell", + f"$shortcut = $shell.CreateShortcut({_ps_single_quote(shortcut_path)})", + f"$shortcut.TargetPath = {_ps_single_quote(chrome_path)}", + f"$shortcut.Arguments = {_ps_single_quote(arguments)}", + f"$shortcut.WorkingDirectory = {_ps_single_quote(working_directory)}", + f"$shortcut.IconLocation = {_ps_single_quote(chrome_path)}", + "$shortcut.Save()", + ] + ) + try: + subprocess.run( + [ + "powershell", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + script, + ], + check=True, + capture_output=True, + text=True, + ) + except OSError as exc: + raise ChromeLaunchError(f"生成快捷方式失败: {exc}") from exc + except subprocess.CalledProcessError as exc: + error = (exc.stderr or exc.stdout or str(exc)).strip() + raise ChromeLaunchError(f"生成快捷方式失败: {error}") from exc + return shortcut_path + + def launch_chrome(account, config=None) -> subprocess.Popen: """Launch Chrome for one account and return the process handle.""" diff --git a/app/gui.py b/app/gui.py index baf9258..d8fd3d6 100644 --- a/app/gui.py +++ b/app/gui.py @@ -208,6 +208,7 @@ if QT_IMPORT_ERROR is None: self.delete_button = QPushButton("删除") self.launch_button = QPushButton("启动登录") self.check_button = QPushButton("检测登录") + self.shortcut_button = QPushButton("快捷方式") toolbar = QHBoxLayout() for button in ( @@ -216,6 +217,7 @@ if QT_IMPORT_ERROR is None: self.delete_button, self.launch_button, self.check_button, + self.shortcut_button, ): toolbar.addWidget(button) toolbar.addStretch(1) @@ -233,6 +235,7 @@ if QT_IMPORT_ERROR is None: self.delete_button.clicked.connect(self.delete_account) self.launch_button.clicked.connect(self.launch_login) self.check_button.clicked.connect(self.check_login) + self.shortcut_button.clicked.connect(self.create_shortcut) self.table.itemSelectionChanged.connect(self._update_button_state) self.table.doubleClicked.connect(self.edit_account) @@ -258,6 +261,7 @@ if QT_IMPORT_ERROR is None: self.delete_button, self.launch_button, self.check_button, + self.shortcut_button, ): button.setEnabled(has_selection) @@ -419,6 +423,18 @@ if QT_IMPORT_ERROR is None: self.refresh_accounts() self._set_status(f"登录检测失败:{error}") + def create_shortcut(self, checked=False): + account = self._selected_account() + if account is None: + return + try: + shortcut_path = accounts.create_shortcut(account, config=self.config) + except Exception as exc: + self._show_error(exc) + return + self._set_status(f"快捷方式已生成:{shortcut_path}") + QMessageBox.information(self, "账号管理", f"快捷方式已生成:\n{shortcut_path}") + class MainWindow(QMainWindow): """Main application window with the fixed five-tab workflow.""" diff --git a/docs/06-tasks.md b/docs/06-tasks.md index f80b4a9..09d3089 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -41,7 +41,7 @@ | T-104 | PySide6 五 Tab 主窗口骨架(`QMainWindow` + `QTabWidget`,5 Tab 空壳) | T-002 | 五个 Tab 按顺序可切换;启动不阻塞;基础状态栏可用 | DONE | | T-104b | PySide6 worker 基类与线程启动工具(`BaseWorker` + `QThread` 包装) | T-104 | signals: progress/log/row_updated/failed/finished/cancelled;取消标记可用;worker 不直接操作 QWidget | DONE | | T-105 | Tab④ 账号增删改(账号名/别名/端口/密码本地明文仅参考)+ 启动登录 + 检测登录 + 顶部 Tab 栏防误点样式 | T-104, T-101, T-103 | 增删改入库、建目录;密码字段 UI 打码;状态列刷新;不自动登录/填密码;5 个 Tab 有足够宽度/间距与当前态高亮,不易误点 | DONE | -| T-106 | 可选:为账号生成桌面快捷方式 | T-102 | `.lnk` 目标含该账号参数;双击进对应账号 | TODO | +| T-106 | 可选:为账号生成桌面快捷方式 | T-102 | `.lnk` 目标含该账号参数;双击进对应账号 | DONE | ## Phase 2 · 导入采集(①) diff --git a/docs/api.md b/docs/api.md index c82219f..56e018a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -140,6 +140,8 @@ update_account(original_alias, account_name, alias, region_host=None, debug_port password=None, note=None, path=None, config=None) -> db.Account delete_account(alias, path=None, config=None) -> None launch_for_login(account_or_alias, path=None, config=None) -> subprocess.Popen +create_shortcut(account_or_alias, shortcut_path=None, desktop_dir=None, + path=None, config=None) -> str detect_login(account_or_alias, timeout=8, path=None, config=None) -> dict login_status_text(status) -> str ``` @@ -150,6 +152,7 @@ login_status_text(status) -> str - `debug_port` 在账号服务层按账号唯一校验;默认端口取 `debug_port_range` 中第一个未占用端口。 - `delete_account()` 只删除 DB 账号记录,不删除本地 user-data-dir,避免误删登录态。 - `launch_for_login()` 只启动带该账号 user-data-dir 和 CDP 端口的 Chrome;不会读取、填写或提交密码。 +- `create_shortcut()` 生成 `.lnk`,目标/参数复用 `chrome.build_launch_args()`,不包含密码。 - `detect_login()` 复用 `editor.login_status()`;检测为已登录时更新 `last_login_at`。 ## chrome 模块(`app/chrome.py`,已建) @@ -159,11 +162,12 @@ class ChromeLaunchError(RuntimeError): ... build_launch_args(account, config=None) -> list[str] # chrome + --remote-debugging-port + --remote-allow-origins=* + --user-data-dir launch_chrome(account, config=None) -> subprocess.Popen +create_shortcut(account, shortcut_path=None, desktop_dir=None, name=None, config=None) -> str wait_debug_ready(port, timeout=60, host="127.0.0.1") -> bool is_running(port, host="127.0.0.1") -> bool ``` -`build_launch_args()` 接受 dict 或对象形式账号;账号需有 `debug_port`,并有 `user_data_dir` 或 `slug/alias`。端口探测访问 `/json/version`,显式禁用环境代理。`.lnk` 快捷方式留给 T-106。 +`build_launch_args()` 接受 dict 或对象形式账号;账号需有 `debug_port`,并有 `user_data_dir` 或 `slug/alias`。端口探测访问 `/json/version`,显式禁用环境代理。`create_shortcut()` 使用 PowerShell `WScript.Shell.CreateShortcut` 生成 `.lnk`,`TargetPath` 为 Chrome,`Arguments` 含 `--remote-debugging-port`、`--remote-allow-origins=*`、`--user-data-dir=<该账号目录>`。 ## cdp 模块(`app/cdp.py`,T-000 由根目录 `cdp.py` 迁入) @@ -251,7 +255,7 @@ render_prompt(template_text, task) -> str # GUI 入口 main() -> int # 创建 QApplication + MainWindow class MainWindow(QMainWindow) # QTabWidget: ①②③④⑤;支持注入 db_path/config 便于测试 -class AccountsTab(QWidget) # ④ 账号管理:表格 + 增删改 + 启动登录 + 检测登录 +class AccountsTab(QWidget) # ④ 账号管理:表格 + 增删改 + 启动登录 + 检测登录 + 快捷方式 class AccountDialog(QDialog) # 账号编辑弹窗;密码 QLineEdit.Password TAB_TITLES: list[str] # 固定 Tab 顺序 TAB_STYLE: str # 顶层 Tab 栏防误点样式:最小宽度/padding/间距/当前态 @@ -265,7 +269,7 @@ TAB_STYLE: str # 顶层 Tab 栏防误点样式: - 表格列:账号名、别名、地区、端口、登录状态、备注;不展示密码。 - 弹窗字段:账号名、别名、地区、调试端口、密码、备注、slug、数据目录;密码框使用打码显示。 -- 「启动登录」只启动 Chrome,人工登录;「检测登录」用 worker 跑 `accounts.detect_login()` 并刷新状态列。 +- 「启动登录」只启动 Chrome,人工登录;「检测登录」用 worker 跑 `accounts.detect_login()` 并刷新状态列;「快捷方式」调用 `accounts.create_shortcut()` 生成默认桌面 `.lnk`。 ## workers 模块(`app/workers.py`,已建,PySide6) diff --git a/docs/current-state.md b/docs/current-state.md index ab8dcd1..ca33c10 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -6,10 +6,10 @@ ## 当前快照 - 日期: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④ 账号管理。 +- 阶段: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 账号快捷方式。 - 技术栈:Python 3.10+,自研 CDP(websocket-client + requests),SQLite(sqlite3)+ `config.json` + openpyxl + AI(服务商待定),GUI PySide6 5 Tab(已定)。 -- 生产代码:已建立 `app/` 包 + 根入口 `main.py`;`app/cdp.py` 为已验证 CDP 底座;`app/editor.py` 已封装登录状态检测、标题/封面/采集/更新按钮能力;`app/appconfig.py` 已实现 `config.json` 默认值/读写/更新、AI 参数与端口读取,以及 `config/ai_models.json` 模型清单 CRUD/过滤/打码/测试连接;`app/db.py` 已实现 SQLite schema、连接 PRAGMA、批次/账号/任务与阶段写库函数;`app/config.py` 已实现账号 slug 与 user-data-dir 创建;`app/accounts.py` 已实现账号 CRUD 服务、端口默认分配、启动登录、检测登录;`app/chrome.py` 已实现 Chrome 参数拼装、启动与 CDP 端口探测;`app/gui.py` 已实现 PySide6 `MainWindow`、五 Tab、顶部 Tab 栏防误点样式、④ 账号管理表格/弹窗/按钮与状态栏;`app/workers.py` 已实现 `BaseWorker`、通用 signals、取消标记和 `QThread` 启动包装。 -- 测试:`tests/` 已建立;T-006 后纯逻辑改动必须运行 `python -m unittest discover -s tests`,当前覆盖 appconfig/db/config/accounts/chrome/editor 登录检测/gui 账号管理/worker signal 与线程包装,并对尚未实现的 app.excel/app.prompts 做契约占位 skip;CDP/Shopee 改动仍需测试商品手动验证。 +- 生产代码:已建立 `app/` 包 + 根入口 `main.py`;`app/cdp.py` 为已验证 CDP 底座;`app/editor.py` 已封装登录状态检测、标题/封面/采集/更新按钮能力;`app/appconfig.py` 已实现 `config.json` 默认值/读写/更新、AI 参数与端口读取,以及 `config/ai_models.json` 模型清单 CRUD/过滤/打码/测试连接;`app/db.py` 已实现 SQLite schema、连接 PRAGMA、批次/账号/任务与阶段写库函数;`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 栏防误点样式、④ 账号管理表格/弹窗/按钮/快捷方式与状态栏;`app/workers.py` 已实现 `BaseWorker`、通用 signals、取消标记和 `QThread` 启动包装。 +- 测试:`tests/` 已建立;T-006 后纯逻辑改动必须运行 `python -m unittest discover -s tests`,当前覆盖 appconfig/db/config/accounts/chrome 启动与快捷方式/editor 登录检测/gui 账号管理/worker signal 与线程包装,并对尚未实现的 app.excel/app.prompts 做契约占位 skip;CDP/Shopee 改动仍需测试商品手动验证。 - 数据:`config.json`、`config/ai_models.json`、`cmshopee.db`、`chrome_user_data_dir/`、`images/` 已由 `.gitignore` 排除;`app/appconfig.py` 首次读取缺失的 `config.json` 时会在本地写默认配置,`app/db.py` 调用 `init_db()` 时会在本地创建 SQLite DB。 ## 既定设计要点(文档已定) @@ -32,14 +32,14 @@ | `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 产出:PySide6 `QMainWindow` + 五 Tab;顶部 Tab 栏防误点样式;④ 账号管理表格、账号弹窗、启动登录、检测登录 | +| `app/gui.py` | 已有 | T-104/T-105/T-106 产出:PySide6 `QMainWindow` + 五 Tab;顶部 Tab 栏防误点样式;④ 账号管理表格、账号弹窗、启动登录、检测登录、快捷方式 | | `app/workers.py` | 已有 | T-104b 产出:`BaseWorker` + 通用 signals + 取消标记 + `run_worker()` QThread 包装 | -| `app/accounts.py` | 已有 | T-105 产出:账号 CRUD 服务、目录创建、端口分配、启动登录、检测登录 | +| `app/accounts.py` | 已有 | T-105/T-106 产出:账号 CRUD 服务、目录创建、端口分配、启动登录、检测登录、快捷方式 | | `app/editor.py` | 已有 | T-001/T-103 产出:登录状态检测、打开商品页、读/写标题、读/下载封面、上传拖封面、更新按钮、apply_task | | `app/appconfig.py` | 已有 | T-002 产出:`config.json` 默认值、读写、更新、路径/端口/AI 参数读取;拒绝敏感字段写入 | | `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 产出:Chrome 启动参数、`subprocess.Popen` 启动、`/json/version` 端口探测;快捷方式待 T-106 | +| `app/chrome.py` | 已有 | T-102/T-106 产出:Chrome 启动参数、`subprocess.Popen` 启动、`/json/version` 端口探测、PowerShell `.lnk` 快捷方式 | | `tests/` | 已有 | T-006 产出:stdlib unittest 基座;覆盖 appconfig/db/config/accounts/chrome/editor/gui/workers;excel/prompts 模块契约占位测试 | | `app/excel.py` | 待建 | Phase 2 产出 | | `config.json` / `config/ai_models.json` / `cmshopee.db` / `chrome_user_data_dir/` / `images/` | 本地待建,已忽略 | 含配置、密钥、业务、登录态、图片,不提交版本库 | @@ -56,9 +56,9 @@ 任务状态以 [`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(`app/db.py` + 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-000(正式代码包结构)、T-001(`app/editor.py` 模块化)、T-002(`app/appconfig.py` + `config.json`)、T-003(`app/db.py` + 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-106(可选:为账号生成桌面快捷方式)**。 +- 下一个可领取任务:**T-201(`app/excel.py` 导入:解析多文件输入列入库)**。 ## 当前可运行内容 diff --git a/docs/routes.md b/docs/routes.md index bef00d0..c1f7ebd 100644 --- a/docs/routes.md +++ b/docs/routes.md @@ -149,7 +149,7 @@ | `CollectTab(QWidget)` | ① | 导入、任务表、采集、回写 | | `GenerateTab(QWidget)` | ② | 左提示词 + 右筛选/任务列表、双击看新旧封面、开始生成 | | `ApplyTab(QWidget)` | ③ | 已生成任务、开始更新确认、换标题+封面+提交、回写 | -| `AccountsTab(QWidget)` | ④ | 账号增删改、启动登录 | +| `AccountsTab(QWidget)` | ④ | 账号增删改、启动登录、检测登录、生成快捷方式 | | `SettingsTab(QWidget)` | ⑤ | AI/目录/Chrome 配置 | | `TaskTableModel(QAbstractTableModel)` | ①②③ | 任务表格数据模型,供 `QTableView` 使用 | | `BaseWorker(QObject)` | 后台 | 定义 `progress/log/row_updated/failed/finished/cancelled` signals | diff --git a/progress.md b/progress.md index 4456321..cb2581f 100644 --- a/progress.md +++ b/progress.md @@ -352,3 +352,11 @@ - 追加收尾:运行最新 GUI 后发现顶部 5 个 Tab 间距偏紧,容易误点;已在 `MainWindow` 应用全局 `TAB_STYLE`,包含 Tab 最小宽度、padding、间距、hover 与当前态高亮,并补测试防止后续误删。 - 验证:`python -m compileall app main.py tests` 通过;`python -m unittest discover -s tests` 通过;`py -3 -m compileall app main.py tests` 通过;`py -3 -m unittest discover -s tests` 通过;本轮未连接真实 Shopee/CDP 做登录检测实跑,登录检测路径由 mock 覆盖。 - 下一步:按任务看板领取 T-106(可选)。 + +## 【2026-06-27】T-106 账号桌面快捷方式 + +- 状态:DONE +- 变更:`app/chrome.py` 新增 `create_shortcut()`,复用 `build_launch_args()` 生成 Chrome 目标与参数,并通过 PowerShell `WScript.Shell.CreateShortcut` 写 `.lnk`;`app/accounts.py` 新增 `create_shortcut()` 服务包装;`app/gui.py` 在 ④ 账号管理增加「快捷方式」按钮,选中账号后生成默认桌面快捷方式并提示路径;同步 `docs/06-tasks.md`、`docs/current-state.md`、`docs/api.md`、`docs/routes.md`。 +- 细节:快捷方式参数包含 `--remote-debugging-port`、`--remote-allow-origins=*`、`--user-data-dir=<该账号目录>`;不包含账号密码/API Key;测试 mock PowerShell 调用,不在测试环境真实写桌面。 +- 验证:`python -m compileall app main.py tests` 通过;`python -m unittest discover -s tests` 通过;`py -3 -m compileall app main.py tests` 通过;`py -3 -m unittest discover -s tests` 通过。 +- 下一步:按任务看板领取 T-201。 diff --git a/tests/test_accounts.py b/tests/test_accounts.py index 990b821..da0a21b 100644 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -90,6 +90,32 @@ class AccountsTests(TempDirMixin, unittest.TestCase): self.assert_removed(temp_dir) + def test_create_shortcut_uses_account_record(self): + with self.make_temp_dir() as temp_dir: + cfg = self.make_config(temp_dir) + account = accounts.create_account("主店", "alias", debug_port=9222, config=cfg) + shortcut_path = os.path.join(temp_dir, "Desktop", "主店.lnk") + + with mock.patch( + "app.accounts.chrome.create_shortcut", + return_value=shortcut_path, + ) as create_shortcut: + result = accounts.create_shortcut( + "alias", + shortcut_path=shortcut_path, + config=cfg, + ) + + self.assertEqual(shortcut_path, result) + create_shortcut.assert_called_once_with( + account, + shortcut_path=shortcut_path, + desktop_dir=None, + config=cfg, + ) + + self.assert_removed(temp_dir) + def test_detect_login_updates_last_login_at_when_logged_in(self): with self.make_temp_dir() as temp_dir: cfg = self.make_config(temp_dir) diff --git a/tests/test_chrome.py b/tests/test_chrome.py index 11085b4..17f6b46 100644 --- a/tests/test_chrome.py +++ b/tests/test_chrome.py @@ -111,6 +111,38 @@ class ChromeTests(TempDirMixin, unittest.TestCase): self.assert_removed(temp_dir) + def test_create_shortcut_writes_lnk_with_required_chrome_arguments(self): + with self.make_temp_dir() as temp_dir: + chrome_path = os.path.join(temp_dir, "Chrome App", "chrome.exe") + shortcut_path = os.path.join(temp_dir, "Desktop", "主店.lnk") + account = { + "alias": "alias", + "debug_port": 9222, + "user_data_dir": os.path.join(temp_dir, "profile with space"), + } + cfg = {"chrome_path": chrome_path} + + with mock.patch("app.chrome.subprocess.run") as run: + result = chrome.create_shortcut( + account, + shortcut_path=shortcut_path, + config=cfg, + ) + + self.assertEqual(os.path.abspath(shortcut_path), result) + run.assert_called_once() + command = run.call_args[0][0] + self.assertEqual("powershell", command[0]) + script = command[-1] + self.assertIn("$shortcut.TargetPath", script) + self.assertIn(chrome_path, script) + self.assertIn("--remote-debugging-port=9222", script) + self.assertIn("--remote-allow-origins=*", script) + self.assertIn("--user-data-dir=", script) + self.assertIn("profile with space", script) + + self.assert_removed(temp_dir) + def test_is_running_and_wait_debug_ready(self): port = self.start_json_version_server() diff --git a/tests/test_gui.py b/tests/test_gui.py index 445471d..9923f0f 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -1,6 +1,7 @@ import unittest import os import sys +from unittest import mock os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") sys.path.insert(0, os.path.dirname(__file__)) @@ -126,6 +127,28 @@ class GuiTests(TempDirMixin, unittest.TestCase): self.assert_removed(temp_dir) + def test_accounts_tab_create_shortcut_for_selected_account(self): + with self.make_temp_dir() as temp_dir: + cfg = self.make_config(temp_dir) + account = accounts.create_account("主店", "alias", debug_port=9222, config=cfg) + statuses = [] + tab = AccountsTab(config=cfg, status_callback=statuses.append) + self.addCleanup(tab.close) + tab.table.selectRow(0) + shortcut_path = os.path.join(temp_dir, "Desktop", "主店.lnk") + + with mock.patch( + "app.gui.accounts.create_shortcut", + return_value=shortcut_path, + ) as create_shortcut, mock.patch("app.gui.QMessageBox.information") as info: + tab.create_shortcut() + + create_shortcut.assert_called_once_with(account, config=cfg) + info.assert_called_once() + self.assertIn(shortcut_path, statuses[-1]) + + self.assert_removed(temp_dir) + if __name__ == "__main__": unittest.main()