Showing
4 changed files
with
190 additions
and
51 deletions
| @@ -2,14 +2,17 @@ | @@ -2,14 +2,17 @@ | ||
| 2 | 2 | ||
| 3 | from __future__ import annotations | 3 | from __future__ import annotations |
| 4 | 4 | ||
| 5 | +import contextlib | ||
| 5 | import json | 6 | import json |
| 6 | import logging | 7 | import logging |
| 7 | import os | 8 | import os |
| 8 | import platform | 9 | import platform |
| 9 | import shutil | 10 | import shutil |
| 10 | -import signal | 11 | +import socket |
| 11 | import subprocess | 12 | import subprocess |
| 13 | +import sys | ||
| 12 | import time | 14 | import time |
| 15 | +from pathlib import Path | ||
| 13 | 16 | ||
| 14 | from xhs.stealth import STEALTH_ARGS | 17 | from xhs.stealth import STEALTH_ARGS |
| 15 | 18 | ||
| @@ -18,6 +21,9 @@ logger = logging.getLogger(__name__) | @@ -18,6 +21,9 @@ logger = logging.getLogger(__name__) | ||
| 18 | # 默认远程调试端口 | 21 | # 默认远程调试端口 |
| 19 | DEFAULT_PORT = 9222 | 22 | DEFAULT_PORT = 9222 |
| 20 | 23 | ||
| 24 | +# 全局进程追踪 | ||
| 25 | +_chrome_process: subprocess.Popen | None = None | ||
| 26 | + | ||
| 21 | # 各平台 Chrome 默认路径 | 27 | # 各平台 Chrome 默认路径 |
| 22 | _CHROME_PATHS: dict[str, list[str]] = { | 28 | _CHROME_PATHS: dict[str, list[str]] = { |
| 23 | "Darwin": [ | 29 | "Darwin": [ |
| @@ -38,6 +44,22 @@ _CHROME_PATHS: dict[str, list[str]] = { | @@ -38,6 +44,22 @@ _CHROME_PATHS: dict[str, list[str]] = { | ||
| 38 | } | 44 | } |
| 39 | 45 | ||
| 40 | 46 | ||
| 47 | +def _get_default_data_dir() -> str: | ||
| 48 | + """返回默认 Chrome Profile 目录路径。""" | ||
| 49 | + return str(Path.home() / ".xhs" / "chrome-profile") | ||
| 50 | + | ||
| 51 | + | ||
| 52 | +def is_port_open(port: int, host: str = "127.0.0.1") -> bool: | ||
| 53 | + """TCP socket 级端口检测(秒级响应)。""" | ||
| 54 | + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: | ||
| 55 | + s.settimeout(1) | ||
| 56 | + try: | ||
| 57 | + s.connect((host, port)) | ||
| 58 | + return True | ||
| 59 | + except (ConnectionRefusedError, TimeoutError, OSError): | ||
| 60 | + return False | ||
| 61 | + | ||
| 62 | + | ||
| 41 | def find_chrome() -> str | None: | 63 | def find_chrome() -> str | None: |
| 42 | """查找 Chrome 可执行文件路径。""" | 64 | """查找 Chrome 可执行文件路径。""" |
| 43 | # 环境变量优先 | 65 | # 环境变量优先 |
| @@ -45,13 +67,28 @@ def find_chrome() -> str | None: | @@ -45,13 +67,28 @@ def find_chrome() -> str | None: | ||
| 45 | if env_path and os.path.isfile(env_path): | 67 | if env_path and os.path.isfile(env_path): |
| 46 | return env_path | 68 | return env_path |
| 47 | 69 | ||
| 48 | - # which/where 查找 | ||
| 49 | - chrome = shutil.which("google-chrome") or shutil.which("chromium") | 70 | + # which/where 查找(含 Windows chrome.exe) |
| 71 | + chrome = ( | ||
| 72 | + shutil.which("google-chrome") | ||
| 73 | + or shutil.which("chromium") | ||
| 74 | + or shutil.which("chrome") | ||
| 75 | + or shutil.which("chrome.exe") | ||
| 76 | + ) | ||
| 50 | if chrome: | 77 | if chrome: |
| 51 | return chrome | 78 | return chrome |
| 52 | 79 | ||
| 53 | # 平台默认路径 | 80 | # 平台默认路径 |
| 54 | system = platform.system() | 81 | system = platform.system() |
| 82 | + | ||
| 83 | + # Windows: 额外检查环境变量路径 | ||
| 84 | + if system == "Windows": | ||
| 85 | + for env_var in ("PROGRAMFILES", "PROGRAMFILES(X86)", "LOCALAPPDATA"): | ||
| 86 | + base = os.environ.get(env_var, "") | ||
| 87 | + if base: | ||
| 88 | + candidate = os.path.join(base, "Google", "Chrome", "Application", "chrome.exe") | ||
| 89 | + if os.path.isfile(candidate): | ||
| 90 | + return candidate | ||
| 91 | + | ||
| 55 | for path in _CHROME_PATHS.get(system, []): | 92 | for path in _CHROME_PATHS.get(system, []): |
| 56 | if os.path.isfile(path): | 93 | if os.path.isfile(path): |
| 57 | return path | 94 | return path |
| @@ -59,55 +96,70 @@ def find_chrome() -> str | None: | @@ -59,55 +96,70 @@ def find_chrome() -> str | None: | ||
| 59 | return None | 96 | return None |
| 60 | 97 | ||
| 61 | 98 | ||
| 99 | +def is_chrome_running(port: int = DEFAULT_PORT) -> bool: | ||
| 100 | + """检查指定端口的 Chrome 是否在运行(TCP 级检测)。""" | ||
| 101 | + return is_port_open(port) | ||
| 102 | + | ||
| 103 | + | ||
| 62 | def launch_chrome( | 104 | def launch_chrome( |
| 63 | port: int = DEFAULT_PORT, | 105 | port: int = DEFAULT_PORT, |
| 64 | headless: bool = False, | 106 | headless: bool = False, |
| 65 | user_data_dir: str | None = None, | 107 | user_data_dir: str | None = None, |
| 66 | chrome_bin: str | None = None, | 108 | chrome_bin: str | None = None, |
| 67 | -) -> subprocess.Popen: | 109 | +) -> subprocess.Popen | None: |
| 68 | """启动 Chrome 进程(带远程调试端口)。 | 110 | """启动 Chrome 进程(带远程调试端口)。 |
| 69 | 111 | ||
| 70 | Args: | 112 | Args: |
| 71 | port: 远程调试端口。 | 113 | port: 远程调试端口。 |
| 72 | headless: 是否无头模式。 | 114 | headless: 是否无头模式。 |
| 73 | - user_data_dir: 用户数据目录(Profile 隔离)。 | 115 | + user_data_dir: 用户数据目录(Profile 隔离),默认 ~/.xhs/chrome-profile。 |
| 74 | chrome_bin: Chrome 可执行文件路径。 | 116 | chrome_bin: Chrome 可执行文件路径。 |
| 75 | 117 | ||
| 76 | Returns: | 118 | Returns: |
| 77 | - Chrome 子进程。 | 119 | + Chrome 子进程,若已在运行则返回 None。 |
| 78 | 120 | ||
| 79 | Raises: | 121 | Raises: |
| 80 | FileNotFoundError: 未找到 Chrome。 | 122 | FileNotFoundError: 未找到 Chrome。 |
| 81 | """ | 123 | """ |
| 124 | + global _chrome_process | ||
| 125 | + | ||
| 126 | + # 已在运行则跳过 | ||
| 127 | + if is_port_open(port): | ||
| 128 | + logger.info("Chrome 已在运行 (port=%d),跳过启动", port) | ||
| 129 | + return None | ||
| 130 | + | ||
| 82 | if not chrome_bin: | 131 | if not chrome_bin: |
| 83 | chrome_bin = find_chrome() | 132 | chrome_bin = find_chrome() |
| 84 | if not chrome_bin: | 133 | if not chrome_bin: |
| 85 | raise FileNotFoundError("未找到 Chrome,请设置 CHROME_BIN 环境变量或安装 Chrome") | 134 | raise FileNotFoundError("未找到 Chrome,请设置 CHROME_BIN 环境变量或安装 Chrome") |
| 86 | 135 | ||
| 136 | + # 默认 user-data-dir | ||
| 137 | + if not user_data_dir: | ||
| 138 | + user_data_dir = _get_default_data_dir() | ||
| 139 | + | ||
| 87 | args = [ | 140 | args = [ |
| 88 | chrome_bin, | 141 | chrome_bin, |
| 89 | f"--remote-debugging-port={port}", | 142 | f"--remote-debugging-port={port}", |
| 143 | + f"--user-data-dir={user_data_dir}", | ||
| 90 | *STEALTH_ARGS, | 144 | *STEALTH_ARGS, |
| 91 | ] | 145 | ] |
| 92 | 146 | ||
| 93 | if headless: | 147 | if headless: |
| 94 | args.append("--headless=new") | 148 | args.append("--headless=new") |
| 95 | 149 | ||
| 96 | - if user_data_dir: | ||
| 97 | - args.append(f"--user-data-dir={user_data_dir}") | ||
| 98 | - | ||
| 99 | # 代理 | 150 | # 代理 |
| 100 | proxy = os.getenv("XHS_PROXY") | 151 | proxy = os.getenv("XHS_PROXY") |
| 101 | if proxy: | 152 | if proxy: |
| 102 | args.append(f"--proxy-server={proxy}") | 153 | args.append(f"--proxy-server={proxy}") |
| 103 | logger.info("使用代理: %s", _mask_proxy(proxy)) | 154 | logger.info("使用代理: %s", _mask_proxy(proxy)) |
| 104 | 155 | ||
| 105 | - logger.info("启动 Chrome: port=%d, headless=%s", port, headless) | 156 | + logger.info("启动 Chrome: port=%d, headless=%s, profile=%s", port, headless, user_data_dir) |
| 106 | process = subprocess.Popen( | 157 | process = subprocess.Popen( |
| 107 | args, | 158 | args, |
| 108 | stdout=subprocess.DEVNULL, | 159 | stdout=subprocess.DEVNULL, |
| 109 | stderr=subprocess.DEVNULL, | 160 | stderr=subprocess.DEVNULL, |
| 110 | ) | 161 | ) |
| 162 | + _chrome_process = process | ||
| 111 | 163 | ||
| 112 | # 等待 Chrome 准备就绪 | 164 | # 等待 Chrome 准备就绪 |
| 113 | _wait_for_chrome(port) | 165 | _wait_for_chrome(port) |
| @@ -120,7 +172,7 @@ def close_chrome(process: subprocess.Popen) -> None: | @@ -120,7 +172,7 @@ def close_chrome(process: subprocess.Popen) -> None: | ||
| 120 | return | 172 | return |
| 121 | 173 | ||
| 122 | try: | 174 | try: |
| 123 | - process.send_signal(signal.SIGTERM) | 175 | + process.terminate() |
| 124 | process.wait(timeout=5) | 176 | process.wait(timeout=5) |
| 125 | except (subprocess.TimeoutExpired, OSError): | 177 | except (subprocess.TimeoutExpired, OSError): |
| 126 | process.kill() | 178 | process.kill() |
| @@ -129,29 +181,20 @@ def close_chrome(process: subprocess.Popen) -> None: | @@ -129,29 +181,20 @@ def close_chrome(process: subprocess.Popen) -> None: | ||
| 129 | logger.info("Chrome 进程已关闭") | 181 | logger.info("Chrome 进程已关闭") |
| 130 | 182 | ||
| 131 | 183 | ||
| 132 | -def is_chrome_running(port: int = DEFAULT_PORT) -> bool: | ||
| 133 | - """检查指定端口的 Chrome 是否在运行。""" | ||
| 134 | - import requests | ||
| 135 | - | ||
| 136 | - try: | ||
| 137 | - resp = requests.get(f"http://127.0.0.1:{port}/json/version", timeout=2) | ||
| 138 | - return resp.status_code == 200 | ||
| 139 | - except (requests.ConnectionError, requests.Timeout): | ||
| 140 | - return False | ||
| 141 | - | ||
| 142 | - | ||
| 143 | def kill_chrome(port: int = DEFAULT_PORT) -> None: | 184 | def kill_chrome(port: int = DEFAULT_PORT) -> None: |
| 144 | """关闭指定端口的 Chrome 实例。 | 185 | """关闭指定端口的 Chrome 实例。 |
| 145 | 186 | ||
| 146 | - 尝试通过 CDP Browser.close 命令关闭,失败则使用进程信号。 | 187 | + 策略: CDP Browser.close → terminate 追踪进程 → 端口查找终止进程。 |
| 147 | 188 | ||
| 148 | Args: | 189 | Args: |
| 149 | port: Chrome 调试端口。 | 190 | port: Chrome 调试端口。 |
| 150 | """ | 191 | """ |
| 151 | - import requests | 192 | + global _chrome_process |
| 152 | 193 | ||
| 153 | # 策略1: 通过 CDP 关闭 | 194 | # 策略1: 通过 CDP 关闭 |
| 154 | try: | 195 | try: |
| 196 | + import requests | ||
| 197 | + | ||
| 155 | resp = requests.get(f"http://127.0.0.1:{port}/json/version", timeout=2) | 198 | resp = requests.get(f"http://127.0.0.1:{port}/json/version", timeout=2) |
| 156 | if resp.status_code == 200: | 199 | if resp.status_code == 200: |
| 157 | ws_url = resp.json().get("webSocketDebuggerUrl") | 200 | ws_url = resp.json().get("webSocketDebuggerUrl") |
| @@ -163,32 +206,70 @@ def kill_chrome(port: int = DEFAULT_PORT) -> None: | @@ -163,32 +206,70 @@ def kill_chrome(port: int = DEFAULT_PORT) -> None: | ||
| 163 | ws.close() | 206 | ws.close() |
| 164 | logger.info("通过 CDP Browser.close 关闭 Chrome (port=%d)", port) | 207 | logger.info("通过 CDP Browser.close 关闭 Chrome (port=%d)", port) |
| 165 | time.sleep(1) | 208 | time.sleep(1) |
| 166 | - return | ||
| 167 | except Exception: | 209 | except Exception: |
| 168 | pass | 210 | pass |
| 169 | 211 | ||
| 170 | - # 策略2: 通过 lsof 查找并 kill 进程 | ||
| 171 | - try: | ||
| 172 | - result = subprocess.run( | ||
| 173 | - ["lsof", "-ti", f":{port}"], | ||
| 174 | - capture_output=True, | ||
| 175 | - text=True, | ||
| 176 | - timeout=5, | ||
| 177 | - ) | ||
| 178 | - if result.returncode == 0 and result.stdout.strip(): | ||
| 179 | - import contextlib | ||
| 180 | - | ||
| 181 | - pids = result.stdout.strip().split("\n") | 212 | + # 策略2: terminate 追踪的子进程 |
| 213 | + if _chrome_process and _chrome_process.poll() is None: | ||
| 214 | + try: | ||
| 215 | + _chrome_process.terminate() | ||
| 216 | + _chrome_process.wait(timeout=5) | ||
| 217 | + logger.info("通过 terminate 关闭追踪的 Chrome 进程") | ||
| 218 | + except Exception: | ||
| 219 | + with contextlib.suppress(Exception): | ||
| 220 | + _chrome_process.kill() | ||
| 221 | + _chrome_process = None | ||
| 222 | + | ||
| 223 | + # 策略3: 通过端口查找并终止进程(跨平台) | ||
| 224 | + if is_port_open(port): | ||
| 225 | + pids = _find_pids_by_port(port) | ||
| 226 | + if pids: | ||
| 182 | for pid in pids: | 227 | for pid in pids: |
| 183 | - with contextlib.suppress(OSError, ValueError): | ||
| 184 | - os.kill(int(pid), signal.SIGTERM) | ||
| 185 | - logger.info("通过 SIGTERM 关闭 Chrome 进程 (port=%d)", port) | ||
| 186 | - time.sleep(1) | 228 | + _kill_pid(pid) |
| 229 | + logger.info("通过进程终止关闭 Chrome (port=%d)", port) | ||
| 230 | + | ||
| 231 | + # 等待端口释放(最多 5s) | ||
| 232 | + deadline = time.monotonic() + 5 | ||
| 233 | + while time.monotonic() < deadline: | ||
| 234 | + if not is_port_open(port): | ||
| 187 | return | 235 | return |
| 188 | - except Exception: | ||
| 189 | - pass | 236 | + time.sleep(0.5) |
| 237 | + | ||
| 238 | + if is_port_open(port): | ||
| 239 | + logger.warning("端口 %d 仍被占用,kill 可能未完全生效", port) | ||
| 240 | + | ||
| 190 | 241 | ||
| 191 | - logger.warning("未能关闭 Chrome (port=%d)", port) | 242 | +def ensure_chrome( |
| 243 | + port: int = DEFAULT_PORT, | ||
| 244 | + headless: bool = False, | ||
| 245 | + user_data_dir: str | None = None, | ||
| 246 | + chrome_bin: str | None = None, | ||
| 247 | +) -> bool: | ||
| 248 | + """确保 Chrome 在指定端口可用(一站式入口)。 | ||
| 249 | + | ||
| 250 | + 如果 Chrome 已在运行,直接返回 True。 | ||
| 251 | + 否则尝试启动 Chrome 并等待端口就绪。 | ||
| 252 | + | ||
| 253 | + Args: | ||
| 254 | + port: 远程调试端口。 | ||
| 255 | + headless: 是否无头模式(仅新启动时生效)。 | ||
| 256 | + user_data_dir: 用户数据目录。 | ||
| 257 | + chrome_bin: Chrome 可执行文件路径。 | ||
| 258 | + | ||
| 259 | + Returns: | ||
| 260 | + True 表示 Chrome 可用,False 表示启动失败。 | ||
| 261 | + """ | ||
| 262 | + if is_port_open(port): | ||
| 263 | + return True | ||
| 264 | + | ||
| 265 | + try: | ||
| 266 | + launch_chrome( | ||
| 267 | + port=port, headless=headless, user_data_dir=user_data_dir, chrome_bin=chrome_bin, | ||
| 268 | + ) | ||
| 269 | + return is_port_open(port) | ||
| 270 | + except FileNotFoundError as e: | ||
| 271 | + logger.error("启动 Chrome 失败: %s", e) | ||
| 272 | + return False | ||
| 192 | 273 | ||
| 193 | 274 | ||
| 194 | def restart_chrome( | 275 | def restart_chrome( |
| @@ -196,7 +277,7 @@ def restart_chrome( | @@ -196,7 +277,7 @@ def restart_chrome( | ||
| 196 | headless: bool = False, | 277 | headless: bool = False, |
| 197 | user_data_dir: str | None = None, | 278 | user_data_dir: str | None = None, |
| 198 | chrome_bin: str | None = None, | 279 | chrome_bin: str | None = None, |
| 199 | -) -> subprocess.Popen: | 280 | +) -> subprocess.Popen | None: |
| 200 | """重启 Chrome:关闭当前实例后以新模式重新启动。 | 281 | """重启 Chrome:关闭当前实例后以新模式重新启动。 |
| 201 | 282 | ||
| 202 | Args: | 283 | Args: |
| @@ -206,7 +287,7 @@ def restart_chrome( | @@ -206,7 +287,7 @@ def restart_chrome( | ||
| 206 | chrome_bin: Chrome 可执行文件路径。 | 287 | chrome_bin: Chrome 可执行文件路径。 |
| 207 | 288 | ||
| 208 | Returns: | 289 | Returns: |
| 209 | - 新的 Chrome 子进程。 | 290 | + 新的 Chrome 子进程,或 None。 |
| 210 | """ | 291 | """ |
| 211 | logger.info("重启 Chrome: port=%d, headless=%s", port, headless) | 292 | logger.info("重启 Chrome: port=%d, headless=%s", port, headless) |
| 212 | kill_chrome(port) | 293 | kill_chrome(port) |
| @@ -220,16 +301,70 @@ def restart_chrome( | @@ -220,16 +301,70 @@ def restart_chrome( | ||
| 220 | 301 | ||
| 221 | 302 | ||
| 222 | def _wait_for_chrome(port: int, timeout: float = 15.0) -> None: | 303 | def _wait_for_chrome(port: int, timeout: float = 15.0) -> None: |
| 223 | - """等待 Chrome 调试端口就绪。""" | 304 | + """等待 Chrome 调试端口就绪(TCP 级检测)。""" |
| 224 | deadline = time.monotonic() + timeout | 305 | deadline = time.monotonic() + timeout |
| 225 | while time.monotonic() < deadline: | 306 | while time.monotonic() < deadline: |
| 226 | - if is_chrome_running(port): | 307 | + if is_port_open(port): |
| 227 | logger.info("Chrome 已就绪 (port=%d)", port) | 308 | logger.info("Chrome 已就绪 (port=%d)", port) |
| 228 | return | 309 | return |
| 229 | time.sleep(0.5) | 310 | time.sleep(0.5) |
| 230 | logger.warning("等待 Chrome 就绪超时 (port=%d)", port) | 311 | logger.warning("等待 Chrome 就绪超时 (port=%d)", port) |
| 231 | 312 | ||
| 232 | 313 | ||
| 314 | +def _find_pids_by_port(port: int) -> list[int]: | ||
| 315 | + """查找占用指定端口的进程 PID(跨平台)。""" | ||
| 316 | + try: | ||
| 317 | + if sys.platform == "win32": | ||
| 318 | + result = subprocess.run( | ||
| 319 | + ["netstat", "-ano", "-p", "TCP"], | ||
| 320 | + capture_output=True, | ||
| 321 | + text=True, | ||
| 322 | + timeout=5, | ||
| 323 | + ) | ||
| 324 | + if result.returncode != 0: | ||
| 325 | + return [] | ||
| 326 | + pids: list[int] = [] | ||
| 327 | + for line in result.stdout.splitlines(): | ||
| 328 | + if f":{port}" in line and "LISTENING" in line: | ||
| 329 | + parts = line.split() | ||
| 330 | + with contextlib.suppress(ValueError, IndexError): | ||
| 331 | + pids.append(int(parts[-1])) | ||
| 332 | + return list(set(pids)) | ||
| 333 | + else: | ||
| 334 | + result = subprocess.run( | ||
| 335 | + ["lsof", "-ti", f":{port}"], | ||
| 336 | + capture_output=True, | ||
| 337 | + text=True, | ||
| 338 | + timeout=5, | ||
| 339 | + ) | ||
| 340 | + if result.returncode != 0 or not result.stdout.strip(): | ||
| 341 | + return [] | ||
| 342 | + pids = [] | ||
| 343 | + for p in result.stdout.strip().split("\n"): | ||
| 344 | + with contextlib.suppress(ValueError): | ||
| 345 | + pids.append(int(p)) | ||
| 346 | + return pids | ||
| 347 | + except Exception: | ||
| 348 | + return [] | ||
| 349 | + | ||
| 350 | + | ||
| 351 | +def _kill_pid(pid: int) -> None: | ||
| 352 | + """终止指定 PID 的进程(跨平台)。""" | ||
| 353 | + try: | ||
| 354 | + if sys.platform == "win32": | ||
| 355 | + subprocess.run( | ||
| 356 | + ["taskkill", "/PID", str(pid), "/F"], | ||
| 357 | + capture_output=True, | ||
| 358 | + timeout=5, | ||
| 359 | + ) | ||
| 360 | + else: | ||
| 361 | + import signal | ||
| 362 | + | ||
| 363 | + os.kill(pid, signal.SIGTERM) | ||
| 364 | + except Exception: | ||
| 365 | + logger.debug("终止进程 %d 失败", pid) | ||
| 366 | + | ||
| 367 | + | ||
| 233 | def _mask_proxy(proxy_url: str) -> str: | 368 | def _mask_proxy(proxy_url: str) -> str: |
| 234 | """隐藏代理 URL 中的敏感信息。""" | 369 | """隐藏代理 URL 中的敏感信息。""" |
| 235 | from urllib.parse import urlparse | 370 | from urllib.parse import urlparse |
| @@ -71,7 +71,7 @@ class RunLock: | @@ -71,7 +71,7 @@ class RunLock: | ||
| 71 | # 检查进程是否存在 | 71 | # 检查进程是否存在 |
| 72 | os.kill(pid, 0) | 72 | os.kill(pid, 0) |
| 73 | return False | 73 | return False |
| 74 | - except (FileNotFoundError, ValueError, ProcessLookupError, PermissionError): | 74 | + except (ValueError, OSError): |
| 75 | return True | 75 | return True |
| 76 | 76 | ||
| 77 | def _force_release(self) -> None: | 77 | def _force_release(self) -> None: |
| @@ -5,6 +5,7 @@ from __future__ import annotations | @@ -5,6 +5,7 @@ from __future__ import annotations | ||
| 5 | import json | 5 | import json |
| 6 | import logging | 6 | import logging |
| 7 | import time | 7 | import time |
| 8 | +from pathlib import Path | ||
| 8 | 9 | ||
| 9 | from .cdp import Page | 10 | from .cdp import Page |
| 10 | from .errors import PublishError | 11 | from .errors import PublishError |
| @@ -217,14 +218,14 @@ def _fill_long_content(page: Page, content: str) -> None: | @@ -217,14 +218,14 @@ def _fill_long_content(page: Page, content: str) -> None: | ||
| 217 | def _insert_images_to_editor(page: Page, image_paths: list[str]) -> None: | 218 | def _insert_images_to_editor(page: Page, image_paths: list[str]) -> None: |
| 218 | """将图片插入到编辑器中。""" | 219 | """将图片插入到编辑器中。""" |
| 219 | for img_path in image_paths: | 220 | for img_path in image_paths: |
| 220 | - normalized = img_path.replace("\\", "/") | 221 | + file_uri = Path(img_path).resolve().as_uri() |
| 221 | page.evaluate( | 222 | page.evaluate( |
| 222 | f""" | 223 | f""" |
| 223 | (() => {{ | 224 | (() => {{ |
| 224 | const editor = document.querySelector({json.dumps(CONTENT_EDITOR)}); | 225 | const editor = document.querySelector({json.dumps(CONTENT_EDITOR)}); |
| 225 | if (!editor) return false; | 226 | if (!editor) return false; |
| 226 | const img = document.createElement('img'); | 227 | const img = document.createElement('img'); |
| 227 | - img.src = 'file:///' + {json.dumps(normalized)}; | 228 | + img.src = {json.dumps(file_uri)}; |
| 228 | editor.appendChild(img); | 229 | editor.appendChild(img); |
| 229 | editor.dispatchEvent(new Event('input', {{ bubbles: true }})); | 230 | editor.dispatchEvent(new Event('input', {{ bubbles: true }})); |
| 230 | return true; | 231 | return true; |
| @@ -159,6 +159,9 @@ class Feed: | @@ -159,6 +159,9 @@ class Feed: | ||
| 159 | "sharedCount": self.note_card.interact_info.shared_count, | 159 | "sharedCount": self.note_card.interact_info.shared_count, |
| 160 | }, | 160 | }, |
| 161 | } | 161 | } |
| 162 | + cover = self.note_card.cover | ||
| 163 | + if cover.url or cover.url_default: | ||
| 164 | + result["cover"] = cover.url or cover.url_default | ||
| 162 | if self.note_card.video: | 165 | if self.note_card.video: |
| 163 | result["video"] = {"duration": self.note_card.video.capa.duration} | 166 | result["video"] = {"duration": self.note_card.video.capa.duration} |
| 164 | return result | 167 | return result |
-
Please register or login to post a comment