From 97d0e08fca70d465d3b44ce9b73fc8cd96a22b37 Mon Sep 17 00:00:00 2001 From: frank Date: Wed, 6 May 2026 22:43:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(login-status):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=A3=80=E6=B5=8B=E5=BE=AE=E4=BF=A1=E7=99=BB=E5=BD=95=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E7=9A=84=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加 login-status 命令,用于检测本机微信客户端的登录状态。 该命令支持 json 和 text 两种输出格式,并返回不同的退出码以区分状态: - 0: 已登录 - 10: 未运行 - 11: 未登录 - 13: 未知状态 实现原理是通过检测 Weixin.exe 进程是否存在,并查找特定的托盘窗口类名来判断登录状态。 --- wechat_cli/commands/login_status.py | 37 ++++++++++++ wechat_cli/core/login_status.py | 93 +++++++++++++++++++++++++++++ wechat_cli/main.py | 6 +- 3 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 wechat_cli/commands/login_status.py create mode 100644 wechat_cli/core/login_status.py diff --git a/wechat_cli/commands/login_status.py b/wechat_cli/commands/login_status.py new file mode 100644 index 0000000..ca3decf --- /dev/null +++ b/wechat_cli/commands/login_status.py @@ -0,0 +1,37 @@ +import click + +from ..core.login_status import detect_login_status +from ..output.formatter import output + + +_EXIT_CODE = { + "logged_in": 0, + "not_running": 10, + "logged_out": 11, + "unknown": 13, +} + +_TEXT = { + "logged_in": "已登录", + "not_running": "未运行", + "logged_out": "未登录", + "unknown": "未知", +} + + +@click.command("login-status") +@click.option("--format", "fmt", default="json", type=click.Choice(["json", "text"]), help="输出格式") +@click.pass_context +def login_status(ctx, fmt): + """检测本机微信是否已登录""" + status = detect_login_status() + if status not in _EXIT_CODE: + status = "unknown" + + if fmt == "json": + output({"status": status}, "json") + else: + output(_TEXT.get(status, "未知"), "text") + + ctx.exit(_EXIT_CODE[status]) + diff --git a/wechat_cli/core/login_status.py b/wechat_cli/core/login_status.py new file mode 100644 index 0000000..8ee9985 --- /dev/null +++ b/wechat_cli/core/login_status.py @@ -0,0 +1,93 @@ +import ctypes +import ctypes.wintypes as wt +import platform +import subprocess + + +_TRAY_TITLE = "WxTrayIconMessageWindow" +_TRAY_CLASS_MARK = "WxTrayIconMessageWindowClass" +_DEFAULT_PROCESS_WINDOWS = "Weixin.exe" + + +def detect_login_status(): + system = platform.system().lower() + if system != "windows": + return "unknown" + + try: + running = _process_exists_windows(_DEFAULT_PROCESS_WINDOWS) + except Exception: + return "unknown" + + if not running: + return "not_running" + + try: + if _has_tray_window_windows(): + return "logged_in" + return "logged_out" + except Exception: + return "unknown" + + +def _process_exists_windows(image_name): + r = subprocess.run( + ["tasklist", "/FI", f"IMAGENAME eq {image_name}", "/FO", "CSV", "/NH"], + capture_output=True, + text=True, + ) + out = (r.stdout or "").strip() + if not out: + return False + lower = out.lower() + if "no tasks are running" in lower or "信息:" in out: + return False + for line in out.splitlines(): + line = line.strip() + if not line: + continue + parts = line.strip('"').split('","') + if parts and parts[0].lower() == image_name.lower(): + return True + return False + + +def _has_tray_window_windows(): + user32 = ctypes.windll.user32 + + EnumWindows = user32.EnumWindows + EnumWindows.argtypes = [ctypes.WINFUNCTYPE(wt.BOOL, wt.HWND, wt.LPARAM), wt.LPARAM] + EnumWindows.restype = wt.BOOL + + GetClassNameW = user32.GetClassNameW + GetClassNameW.argtypes = [wt.HWND, wt.LPWSTR, ctypes.c_int] + GetClassNameW.restype = ctypes.c_int + + GetWindowTextW = user32.GetWindowTextW + GetWindowTextW.argtypes = [wt.HWND, wt.LPWSTR, ctypes.c_int] + GetWindowTextW.restype = ctypes.c_int + + found = {"ok": False} + + @ctypes.WINFUNCTYPE(wt.BOOL, wt.HWND, wt.LPARAM) + def _cb(hwnd, lparam): + class_buf = ctypes.create_unicode_buffer(256) + title_buf = ctypes.create_unicode_buffer(256) + + GetClassNameW(hwnd, class_buf, len(class_buf)) + GetWindowTextW(hwnd, title_buf, len(title_buf)) + + title = title_buf.value or "" + if title != _TRAY_TITLE: + return True + + cls = class_buf.value or "" + if _TRAY_CLASS_MARK in cls: + found["ok"] = True + return False + + return True + + EnumWindows(_cb, 0) + return bool(found["ok"]) + diff --git a/wechat_cli/main.py b/wechat_cli/main.py index 3400c06..02a22d3 100644 --- a/wechat_cli/main.py +++ b/wechat_cli/main.py @@ -29,8 +29,8 @@ def cli(ctx, config_path): wechat-cli contacts --query "李" # 搜索联系人 wechat-cli new-messages # 获取增量新消息 """ - # init/version 命令不需要 AppContext - if ctx.invoked_subcommand in ("init", "version"): + # init/version/login-status 命令不需要 AppContext + if ctx.invoked_subcommand in ("init", "version", "login-status"): return try: @@ -55,6 +55,7 @@ def cli(ctx, config_path): from .commands.stats import stats from .commands.unread import unread from .commands.favorites import favorites +from .commands.login_status import login_status cli.add_command(init) cli.add_command(sessions) @@ -67,6 +68,7 @@ def cli(ctx, config_path): cli.add_command(stats) cli.add_command(unread) cli.add_command(favorites) +cli.add_command(login_status) if __name__ == "__main__":