T-585 Chrome 路径自动检测
This commit is contained in:
+1
-1
@@ -71,7 +71,7 @@ LEGACY_USER_DATA_PATHS = (
|
||||
)
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"chrome_path": r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
||||
"chrome_path": "",
|
||||
"user_data_root": "chrome_user_data_dir",
|
||||
"image_dir": "images",
|
||||
"db_path": "cmshopee.db",
|
||||
|
||||
+155
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.error
|
||||
@@ -15,12 +16,166 @@ from . import config as account_config
|
||||
|
||||
|
||||
CDP_HOST = "127.0.0.1"
|
||||
CHROME_APP_PATHS_KEY = r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe"
|
||||
|
||||
|
||||
class ChromeLaunchError(RuntimeError):
|
||||
"""Raised when Chrome launch configuration is invalid."""
|
||||
|
||||
|
||||
def _normalize_chrome_path(value) -> str:
|
||||
"""Return a filesystem-ready Chrome path, or an empty string."""
|
||||
|
||||
text = str(value or "").strip()
|
||||
if len(text) >= 2 and text[0] == text[-1] and text[0] in {"\"", "'"}:
|
||||
text = text[1:-1].strip()
|
||||
if not text:
|
||||
return ""
|
||||
text = os.path.expandvars(os.path.expanduser(text))
|
||||
return os.path.abspath(text)
|
||||
|
||||
|
||||
def _load_winreg():
|
||||
"""Return winreg only on Windows so non-Windows callers stay harmless."""
|
||||
|
||||
if os.name != "nt":
|
||||
return None
|
||||
try:
|
||||
import winreg
|
||||
except ImportError:
|
||||
return None
|
||||
return winreg
|
||||
|
||||
|
||||
def _registry_view_flags(winreg):
|
||||
flags = [0]
|
||||
for name in ("KEY_WOW64_64KEY", "KEY_WOW64_32KEY"):
|
||||
value = getattr(winreg, name, 0)
|
||||
if value and value not in flags:
|
||||
flags.append(value)
|
||||
return flags
|
||||
|
||||
|
||||
def _close_registry_key(key):
|
||||
close = getattr(key, "Close", None) or getattr(key, "close", None)
|
||||
if callable(close):
|
||||
try:
|
||||
close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _registry_chrome_candidates():
|
||||
"""Yield Chrome App Paths values without exposing registry failures."""
|
||||
|
||||
winreg = _load_winreg()
|
||||
if winreg is None:
|
||||
return
|
||||
|
||||
key_read = getattr(winreg, "KEY_READ", 0)
|
||||
hives = (
|
||||
getattr(winreg, "HKEY_CURRENT_USER", None),
|
||||
getattr(winreg, "HKEY_LOCAL_MACHINE", None),
|
||||
)
|
||||
seen = set()
|
||||
for hive in hives:
|
||||
if hive is None:
|
||||
continue
|
||||
for view_flag in _registry_view_flags(winreg):
|
||||
marker = (hive, view_flag)
|
||||
if marker in seen:
|
||||
continue
|
||||
seen.add(marker)
|
||||
key = None
|
||||
try:
|
||||
key = winreg.OpenKey(
|
||||
hive,
|
||||
CHROME_APP_PATHS_KEY,
|
||||
0,
|
||||
key_read | view_flag,
|
||||
)
|
||||
value, _value_type = winreg.QueryValueEx(key, "")
|
||||
except (AttributeError, OSError):
|
||||
continue
|
||||
finally:
|
||||
if key is not None:
|
||||
_close_registry_key(key)
|
||||
if value:
|
||||
yield value
|
||||
|
||||
|
||||
def _standard_chrome_candidates():
|
||||
"""Yield the supported Windows Chrome install locations."""
|
||||
|
||||
suffix = ("Google", "Chrome", "Application", "chrome.exe")
|
||||
for variable in ("ProgramFiles", "ProgramFiles(x86)", "LOCALAPPDATA"):
|
||||
base = os.environ.get(variable, "").strip()
|
||||
if base:
|
||||
yield os.path.join(base, *suffix)
|
||||
|
||||
|
||||
def is_configured_chrome_path_valid(value) -> bool:
|
||||
"""Return whether a configured Chrome executable can be launched safely."""
|
||||
|
||||
normalized = _normalize_chrome_path(value)
|
||||
if normalized and os.path.isfile(normalized):
|
||||
return True
|
||||
text = str(value or "").strip().strip("\"")
|
||||
return text.lower() == "chrome.exe" and bool(shutil.which(text))
|
||||
|
||||
|
||||
def detect_chrome_path() -> str:
|
||||
"""Find an installed Google Chrome executable without probing other browsers."""
|
||||
|
||||
for candidate in _registry_chrome_candidates():
|
||||
normalized = _normalize_chrome_path(candidate)
|
||||
if normalized and os.path.isfile(normalized):
|
||||
return normalized
|
||||
for candidate in _standard_chrome_candidates():
|
||||
normalized = _normalize_chrome_path(candidate)
|
||||
if normalized and os.path.isfile(normalized):
|
||||
return normalized
|
||||
path_candidate = shutil.which("chrome.exe")
|
||||
normalized = _normalize_chrome_path(path_candidate)
|
||||
if normalized and os.path.isfile(normalized):
|
||||
return normalized
|
||||
return ""
|
||||
|
||||
|
||||
def ensure_configured_chrome_path(config=None, config_path=None) -> dict:
|
||||
"""Persist an auto-detected path only when the configured path is unusable."""
|
||||
|
||||
path = config_path or (config or {}).get("config_path") or appconfig.CONFIG_PATH
|
||||
current = appconfig.load_config(path) if config is None else config
|
||||
configured_path = appconfig.chrome_path(current)
|
||||
if is_configured_chrome_path_valid(configured_path):
|
||||
return {
|
||||
"changed": False,
|
||||
"config": current,
|
||||
"path": configured_path,
|
||||
"message": "",
|
||||
}
|
||||
|
||||
detected_path = detect_chrome_path()
|
||||
if not detected_path:
|
||||
return {
|
||||
"changed": False,
|
||||
"config": current,
|
||||
"path": configured_path,
|
||||
"message": "",
|
||||
}
|
||||
|
||||
updated = dict(current)
|
||||
updated["chrome_path"] = detected_path
|
||||
saved = appconfig.save_config(updated, path=path)
|
||||
return {
|
||||
"changed": True,
|
||||
"config": saved,
|
||||
"path": detected_path,
|
||||
"message": f"已自动定位 Chrome:{detected_path}",
|
||||
}
|
||||
|
||||
|
||||
def _account_value(account, field, default=None):
|
||||
if isinstance(account, dict):
|
||||
return account.get(field, default)
|
||||
|
||||
+10
-2
@@ -6,7 +6,7 @@ import os
|
||||
import sys
|
||||
import webbrowser
|
||||
|
||||
from .. import appconfig, diagnostics, update_check
|
||||
from .. import appconfig, chrome, diagnostics, update_check
|
||||
from ..version import APP_NAME, display_name
|
||||
from . import widgets as _widgets
|
||||
from .widgets import *
|
||||
@@ -136,6 +136,14 @@ def main() -> int:
|
||||
return 1
|
||||
if not _run_startup_update_gate():
|
||||
return 1
|
||||
window = MainWindow()
|
||||
try:
|
||||
startup = chrome.ensure_configured_chrome_path()
|
||||
except (OSError, appconfig.ConfigError) as exc:
|
||||
QMessageBox.critical(None, "启动配置错误", str(exc))
|
||||
return 1
|
||||
window = MainWindow(
|
||||
config=startup["config"],
|
||||
startup_status=startup["message"],
|
||||
)
|
||||
window.show()
|
||||
return app.exec()
|
||||
|
||||
+10
-1
@@ -74,7 +74,14 @@ def _fit_and_center_window(
|
||||
class MainWindow(QMainWindow):
|
||||
"""Main application window with the fixed five-tab workflow."""
|
||||
|
||||
def __init__(self, db_path=None, config=None, config_path=None, ai_models_path=None):
|
||||
def __init__(
|
||||
self,
|
||||
db_path=None,
|
||||
config=None,
|
||||
config_path=None,
|
||||
ai_models_path=None,
|
||||
startup_status="",
|
||||
):
|
||||
super().__init__()
|
||||
self._initial_window_fit_applied_after_show = False
|
||||
self.config = appconfig.load_config(config_path or appconfig.CONFIG_PATH) if config is None else config
|
||||
@@ -104,6 +111,8 @@ class MainWindow(QMainWindow):
|
||||
self.tabs.setTabIcon(TAB_TITLES.index("③ 更新蝦皮"), _warning_dot_icon())
|
||||
self.setCentralWidget(self.tabs)
|
||||
self.show_status("就绪", level="muted")
|
||||
if startup_status:
|
||||
self.show_status(startup_status, level="success")
|
||||
|
||||
def showEvent(self, event):
|
||||
super().showEvent(event)
|
||||
|
||||
@@ -6,6 +6,7 @@ from contextlib import contextmanager
|
||||
import os
|
||||
|
||||
from ... import ai as ai_module
|
||||
from ... import chrome
|
||||
from ..widgets import *
|
||||
from ..workers import AIModelTestWorker as _RealAIModelTestWorker
|
||||
from ..workers import CMHubSettingsWorker as _RealCMHubSettingsWorker
|
||||
@@ -165,6 +166,8 @@ class SettingsTab(QWidget):
|
||||
self.chrome_path_edit.setObjectName("chromePathEdit")
|
||||
self.chrome_path_browse_button = QPushButton("选择...")
|
||||
self.chrome_path_browse_button.setObjectName("chromePathBrowseButton")
|
||||
self.chrome_path_detect_button = QPushButton("自动检测")
|
||||
self.chrome_path_detect_button.setObjectName("chromePathAutoDetectButton")
|
||||
self.chrome_path_widget = QWidget()
|
||||
self.chrome_path_widget.setObjectName("chromePathWidget")
|
||||
chrome_path_layout = QHBoxLayout(self.chrome_path_widget)
|
||||
@@ -172,6 +175,7 @@ class SettingsTab(QWidget):
|
||||
chrome_path_layout.setSpacing(8)
|
||||
chrome_path_layout.addWidget(self.chrome_path_edit, 1)
|
||||
chrome_path_layout.addWidget(self.chrome_path_browse_button)
|
||||
chrome_path_layout.addWidget(self.chrome_path_detect_button)
|
||||
self.user_data_root_edit = QLineEdit()
|
||||
self.user_data_root_edit.setObjectName("userDataRootEdit")
|
||||
self.user_data_root_edit.setEnabled(False)
|
||||
@@ -393,6 +397,7 @@ class SettingsTab(QWidget):
|
||||
self._update_response_timeout_label
|
||||
)
|
||||
self.chrome_path_browse_button.clicked.connect(self.browse_chrome_path)
|
||||
self.chrome_path_detect_button.clicked.connect(self.detect_chrome_path)
|
||||
self.save_config_button.clicked.connect(self.save_app_settings)
|
||||
self._connect_dirty_signals()
|
||||
|
||||
@@ -535,6 +540,17 @@ class SettingsTab(QWidget):
|
||||
return
|
||||
self.chrome_path_edit.setText(path)
|
||||
|
||||
def detect_chrome_path(self, checked=False):
|
||||
path = chrome.detect_chrome_path()
|
||||
if not path:
|
||||
self._set_status(
|
||||
"未找到 Chrome,请点『选择...』手动指定,或先安装 Chrome",
|
||||
level="warning",
|
||||
)
|
||||
return
|
||||
self.chrome_path_edit.setText(path)
|
||||
self._set_status(f"已自动定位 Chrome:{path}", level="success")
|
||||
|
||||
def discard_unsaved_changes(self):
|
||||
try:
|
||||
saved = appconfig.load_config(self.config_path)
|
||||
|
||||
Reference in New Issue
Block a user