#!/usr/bin/env python3 """直接传入数字 UID,通过接口关注;已关注则跳过,不打开目标主页。""" import argparse import json import subprocess import sys from urllib.parse import urlencode from cdp_explicit import add_cdp_args, configure, evaluate_script from get_current_user import get_user_from_browser PROFILE = "/aweme/v1/web/user/profile/other/" FOLLOW = "/aweme/v1/web/commit/follow/user/" PARAMS = {"device_platform": "webapp", "aid": 6383, "channel": "channel_pc_web"} def validate_uid(uid: str) -> str: if ( not isinstance(uid, str) or not uid.isascii() or not uid.isdecimal() or uid.startswith("0") ): raise ValueError("UID 必须为不带前导零的正整数数字字符串,不是主页链接或抖音号") return uid def request(path: str, params: dict, body: dict | None = None) -> dict: url = path + "?" + urlencode({**PARAMS, **params}) expression = """new Promise((resolve, reject) => { if (location.origin !== 'https://www.douyin.com') return reject(new Error('请先选择已登录的抖音标签页')); const xhr = new XMLHttpRequest(); xhr.open(METHOD, URL); xhr.withCredentials = true; xhr.timeout = 20000; if (BODY !== null) xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); xhr.onload = () => resolve(JSON.stringify({status: xhr.status, body: xhr.responseText})); xhr.onerror = () => reject(new Error('网络错误;如已发送关注,请先查询状态,勿自动重试')); xhr.ontimeout = () => reject(new Error('请求超时;如已发送关注,请先查询状态,勿自动重试')); xhr.send(BODY); })""".replace("METHOD", json.dumps("POST" if body is not None else "GET")) expression = expression.replace("URL", json.dumps(url)).replace( "BODY", json.dumps(urlencode(body) if body is not None else None) ) result = evaluate_script(expression) try: response = json.loads(result.stdout.strip().splitlines()[-1]) payload = json.loads(response["body"]) if not isinstance(payload, dict): raise ValueError("响应不是对象") except (ValueError, KeyError, IndexError, TypeError) as exc: raise RuntimeError("无法解析接口响应,停止操作") from exc if response["status"] != 200 or payload.get("status_code") != 0: raise RuntimeError( f"接口失败:HTTP {response['status']},status_code={payload.get('status_code')};" "请检查登录态或风控提示,不会自动登录、重试或处理验证码" ) return payload def get_profile(uid: str) -> dict: user = request(PROFILE, {"user_id": validate_uid(uid)}).get("user") if not isinstance(user, dict) or user.get("uid") != uid: raise RuntimeError("返回用户 UID 与目标不匹配,停止操作") return user def follow_user(uid: str, check_only: bool = False) -> dict: uid = validate_uid(uid) # 复用登录态检查;失败即退出,绝不尝试登录。 try: current = get_user_from_browser() except RuntimeError as exc: raise RuntimeError("无法确认登录状态,请手动检查或登录后再运行") from exc if not current.get("uid") or str(current["uid"]) == "0": raise RuntimeError("登录状态已失效,请手动登录后再运行") if uid == str(current["uid"]): raise ValueError("不能关注自己") user = get_profile(uid) status = user.get("follow_status") if type(status) is not int or status not in (0, 1, 2): raise RuntimeError(f"未知关注状态 {status!r},停止而不发送请求") action = "checked" if check_only else "already_following" if not check_only and status == 0: result = request(FOLLOW, {}, {"user_id": user["uid"], "type": 1}) if type(result.get("follow_status")) is not int or result[ "follow_status" ] not in (1, 2): raise RuntimeError("关注响应未确认成功;请使用 --check 查询,勿自动重试") verified = get_profile(uid) if verified["uid"] != user["uid"] or verified.get("follow_status") not in ( 1, 2, ): raise RuntimeError( "关注请求已发出,但回查未确认;请使用 --check 查询,勿自动重试" ) status = verified["follow_status"] action = "followed" return { "action": action, "uid": user["uid"], "sec_uid": user.get("sec_uid"), "nickname": user.get("nickname"), "follow_status": status, } def main() -> int: parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False) parser.add_argument("uid", help="目标用户数字 UID(通知 users[].uid),不是抖音号") parser.add_argument("--check", action="store_true", help="只查询,不关注") add_cdp_args(parser) args = parser.parse_args() configure(args) try: uid = validate_uid(args.uid) print(json.dumps(follow_user(uid, args.check), ensure_ascii=False, indent=2)) except ( OSError, ValueError, RuntimeError, KeyError, TypeError, subprocess.SubprocessError, ) as exc: # 不回显浏览器 stderr:它可能包含带签名的请求 URL。 message = ( "浏览器连接或执行失败,请检查 CDP 与登录态" if isinstance(exc, subprocess.SubprocessError) else str(exc) ) print(f"失败:{message}。不会自动登录或重试关注。", file=sys.stderr) return 1 return 0 if __name__ == "__main__": raise SystemExit(main())