# -*- coding: utf-8 -*- # Author : Charley # Python : 3.12.10 # Date : 2026/08/21 """卡玩 App token 抓取脚本。 MuMu 模拟器(已 root)里卡玩 App 登录后,登录态 JWT 存在 Flutter 的 SharedPreferences 文件(FlutterSharedPreferences.xml)的 flutter.auth_token 字段里。本脚本经 adb 以 root 身份直接读该文件,抠出当前有效的 token。 该 token 是 Sa-Token 风格的 JWT(loginId=app_user:xxx + rnStr + HS256), 换账号重登会覆盖旧值,所以读到的必然是此刻有效的那一条。请求时按接口实际情况 放进请求头(卡玩大概率是 satoken 字段),本脚本返回裸 token,不拼任何前缀。 用法: python get_token.py # 打印当前 token from get_token import get_token # 爬虫里直接调用,每次取最新 token """ import sys import subprocess # MuMu 12 模拟器的 adb 地址(多开实例端口可能不同,按需改,常见 16384 / 16416 ...) MUMU_ADB_ADDR = "127.0.0.1:16384" # 卡玩 App 包名 PACKAGE_NAME = "com.kawan.kawanapp" # 当前登录态 token 所在的 Flutter SharedPreferences 文件(只存当前账号的一条) PREFS_PATH = f"/data/data/{PACKAGE_NAME}/shared_prefs/FlutterSharedPreferences.xml" # 设备端执行的 grep:只抠 flutter.auth_token 这一项的值 # 原始 XML 形如 TOKEN, # 输出形如 flutter.auth_token">TOKEN,Python 侧再切出 TOKEN REMOTE_GREP_CMD = f"grep -o 'flutter.auth_token\">[^<]*' {PREFS_PATH}" # grep 输出里 token 前的分隔标记(值紧跟在 "> 之后) _VALUE_SEP = '">' def _run(cmd: list[str], timeout: int = 20) -> subprocess.CompletedProcess: """执行一条命令并返回结果对象。 Args: cmd (list[str]): 完整命令 argv 列表,来自各调用处拼好的 adb 命令。 timeout (int, optional): 超时秒数。Defaults to 20。 Returns: subprocess.CompletedProcess: 执行结果,含 returncode / stdout / stderr。 Raises: RuntimeError: 未找到 adb 可执行文件,或命令超时时抛出。 """ try: return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout) except FileNotFoundError as e: # 环境里没有 adb 可执行文件(未装 platform-tools 或未加入 PATH) raise RuntimeError( f"[{sys._getframe().f_code.co_name}] 未找到 adb,请确认已把 platform-tools 加入 PATH" ) from e except subprocess.TimeoutExpired as e: raise RuntimeError( f"[{sys._getframe().f_code.co_name}] adb 命令超时,请检查模拟器与 adb 连接" ) from e def _prepare_device() -> None: """确保已连上 MuMu 并让 adbd 以 root 运行。 两步都幂等:已连接会提示 already connected,已 root 会提示 already running as root,均不影响后续读取。连接 / 提权若失败,不在此中断,交由读取阶段统一暴露。 Returns: None: 无返回值。 """ # 幂等连接:未连则建立,已连直接复用 _run(["adb", "connect", MUMU_ADB_ADDR]) # 幂等提权:MuMu 支持 adb root,读 App 私有目录需要 root 身份 _run(["adb", "-s", MUMU_ADB_ADDR, "root"]) def get_token() -> str: """从已登录的卡玩 App 抓取当前登录态 token。 先确保连上 MuMu 且 adbd 为 root,再以 root 读取 Flutter 的 SharedPreferences,grep 出 flutter.auth_token 的值。整条远程命令作为单个 字符串传给 adb shell,保证单引号在设备端正确解析。 Returns: str: 当前有效的裸 token(Sa-Token 风格 JWT,不含任何前缀)。 Raises: RuntimeError: 设备未连接 / 未 root / App 未登录 / 未抓到 token 时抛出。 """ _prepare_device() # 整条远程命令作为单个字符串传参,避免 > 与 [^<]* 被本机 shell 或设备 shell 误解析 result = _run(["adb", "-s", MUMU_ADB_ADDR, "shell", REMOTE_GREP_CMD]) if result.returncode != 0: # grep 未匹配(App 未登录 / 文件不存在)也会返回非 0,统一当作失败处理 err = result.stderr.strip() or result.stdout.strip() or "未知错误" raise RuntimeError( f"[{sys._getframe().f_code.co_name}] 抓取失败:{err}\n" f"排查:1) 模拟器是否开着且 adb 能连上 {MUMU_ADB_ADDR} " f"2) adb 是否已 root 3) 卡玩 App 是否已登录" ) # 输出形如 flutter.auth_token">eyJ...,切出 "> 之后的 token(.strip 顺带去掉 \r) token = "" for line in result.stdout.splitlines(): line = line.strip() if _VALUE_SEP in line: token = line.split(_VALUE_SEP, 1)[1].strip() if token: break if not token: # 匹配到字段但值为空 = App 已退出登录 / 尚未登录 raise RuntimeError( f"[{sys._getframe().f_code.co_name}] 未抓到 token,请确认卡玩 App 已登录并稳定运行几秒后再试" ) return token if __name__ == "__main__": try: print(get_token()) except RuntimeError as exc: print(exc) sys.exit(1)