首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >让 WorkBuddy 自己领积分:自动化「派猫猫旅行」绕过 JWT 误区、Cookie 鉴权与幂等状态机实战

让 WorkBuddy 自己领积分:自动化「派猫猫旅行」绕过 JWT 误区、Cookie 鉴权与幂等状态机实战

原创
作者头像
用户12683097
修改2026-09-01 18:18:47
修改2026-09-01 18:18:47
110
举报

🐱 用纯 HTTP 脚本自动化 WorkBuddy「派猫猫旅行」:绕过 JWT 误区、Cookie 鉴权与幂等状态机实战

💡 一句话导语:把网页端每日积分小游戏改造成可定时、可跨电脑运行的纯 Python 脚本。重点不在"拿到积分",而在三个真实踩坑和一套状态机 + 幂等设计。所有结论均来自本机实测 HTTP 报文,文末附完整可运行代码(已脱敏)。


🎯 一、背景:为什么要写这个脚本

WorkBuddy 网页端的「成长计划 → 派猫猫旅行」里有个小猫旅行玩法:每天把小猫派去 4 个地点之一(☕ 咖啡馆 / 🛍️ 商场店铺 / 💪 健身房 / 🏮 古镇客栈),旅行 1—4 小时后回来,领取 5—10 积分,每日限一次。下图是实际玩法界面:

玩法简单,但手动操作有两个痛点:

  1. 要在固定时间窗口回来领,否则当天额度浪费;
  2. 多台电脑都要玩,每台重复点一遍很烦。

🎯 目标:写个脚本,定时自动"派出 → 等待 → 领取",并且能直接拷到别的电脑上跑


💥 二、三个致命误区(本文核心干货)

动手前,我凭经验做了三个假设,结果每一个都是错的,逐一实测纠正。

❌ 误区 1:后端在 copilot.tencent.com,鉴权用 JWT / Bearer Token

第一反应是"这是腾讯的 AI 产品,后端在 copilot.tencent.com,请求头里一定有 Authorization: Bearer <token>"。

实测打脸:用 Chrome 登录后抓包,所有 travel/* 请求的真实域名是 www.workbuddy.cn,而且请求头里根本没有 Authorization

❌ 误区 2:DevTools「Copy as fetch」能拿到完整请求

这是最隐蔽的坑。在 Network 面板右键请求 → Copy as fetch,生成的 fetch() 代码里 Cookie 头被 Chrome 主动剥离了(浏览器出于安全考虑,不会在复制出的代码里暴露 Cookie)。

⚠️ 如果只信 Copy as fetch,你会得到一个"永远 401"的请求。 正确做法是:直接复制 Request Headers 面板里 cookie: 这一行的原始文本,而不是复制 fetch 代码。

❌ 误区 3:接口路径带 /v2/

很多腾讯系接口是 /v2/xxx 风格,于是我一开始拼的是 https://www.workbuddy.cn/v2/activity/growth/buddy/travel/status,结果被 APISIX 网关返回 401 Authorization Required

实测真相:真实路径不带 /v2/

下面用一张图把三个误区和实测真相并排看清:


🔍 三、还原后的真实接口

经过以上纠正,最终确认(2026-09-01 实测 HTTP 200):

🌐 域名

https://www.workbuddy.cn

📍 路径前缀

/activity/growth/buddy/travel/(无 /v2/)

🔑 鉴权

HttpOnly Cookie:session / session_2 / tgw_l7_route,无 JWT

📦 业务响应

{ "code": 0, "data": {...} },以 data.state 为权威状态

四个端点:

代码语言:javascript
复制
GET  /activity/growth/buddy/travel/status   # 查当前状态
GET  /activity/growth/buddy/travel/config   # 查可去地点(可选)
POST /activity/growth/buddy/travel/depart   # 派出旅行
POST /activity/growth/buddy/travel/claim    # 领取积分

status 返回的 data 关键字段:

字段

含义

state

idle / traveling / arrived 三态之一

daily_limit_reached

今日是否已领满(布尔)

record_id

本次旅行记录 id(claim 时可能需要)

arrive_at

预计到达(领取)时间戳

reward_credit

本次奖励积分数

location.name

当前/上次旅行地点名

server_now

服务端当前时间戳(用于算剩余时间)


🧠 四、架构设计:状态机 + 幂等

4.1 为什么是"轮询"而不是"一次跑完"

旅行要 1~4 小时,不可能在一个进程里 sleep 几小时。所以设计成:多个定时时刻去"巡检"一次,每次根据服务端真实状态决定动作。脚本本身无状态,服务端 data.state 是唯一权威。

状态机逻辑(幂等都是靠服务端状态保证的):

代码语言:javascript
复制
┌──────────────────────────────────────────────────────────┐
│  GET /status → 读 data.state                               │
│                                                            │
│  state == arrived        → POST /claim   (领积分,只一次) │
│  state == traveling      → 跳过           (绝不重复派)    │
│  daily_limit_reached     → 跳过           (今日已领满)    │
│  其他(idle)            → POST /depart  (随机派一地)     │
└──────────────────────────────────────────────────────────┘

🛡️ 这套设计天然幂等:即使定时器抖动、脚本重复执行,也不会重复派或重复领——因为动作完全由服务端状态驱动。

4.2 参数名多方案回退(容错)

服务端对 depart / claim 接受的参数名没有明确文档。为抗未来字段改名,脚本对每个写操作都做多方案 payload 回退record_id / recordIdlocation_id / locationId / location / location_code),哪一个返回 code:0 就用哪一个。这样接口细节微调也不会直接挂掉。

4.3 手动验证:先跑通才有底气

💡 这一步的意义:证明(a)接口设计跑得通、(b)真实到账不只是前端显示、(c)积分涨幅在预期区间内。自动化只是把这个手动过程变成可循环的脚本。

4.4 纯标准库、零依赖、可移植

脚本只用 urllib(Python 标准库),不依赖 requests 等第三方包,Python 3.8+ 任意环境可跑。所有路径基于 __file__ 相对计算,不写死任何绝对路径,因此整个目录拷到别的电脑就能跑。


💻 五、完整可运行代码(已脱敏)

凭据放在同目录 wb_cookies.txt,内容为从浏览器复制的 cookie: 原始值(见下文部署说明)。脚本完整如下:

代码语言:javascript
复制
# -*- coding: utf-8 -*-
"""派猫猫旅行自动化(cookie 鉴权版,纯标准库,可移植)。

用法:
  python travel_auto.py                # 正常执行一轮
  python travel_auto.py --dry-run      # 只查状态,不做任何写操作
  python travel_auto.py --location 2   # 指定派出地点 id(1咖啡馆 2商场 3健身房 4客栈)
"""
import os
import sys
import json
import time
import random
import argparse
import datetime
import urllib.request
import urllib.error

HERE = os.path.dirname(os.path.abspath(__file__))
COOKIE_FILE = os.path.join(HERE, 'wb_cookies.txt')
LOG_FILE = os.path.join(HERE, 'travel_auto.log')
STATE_FILE = os.path.join(HERE, 'travel_state.json')
BASE = 'https://www.workbuddy.cn/activity/growth/buddy/travel'

UA = ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
      '(KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36')
LOCATIONS = {1: '咖啡馆', 2: '商场店铺', 3: '健身房', 4: '古镇客栈'}
LOC_CODES = {1: 'coffee', 2: 'mall', 3: 'gym', 4: 'inn'}


def ts():
    return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')


def log(line):
    text = '[%s] %s' % (ts(), line)
    print(text)
    try:
        with open(LOG_FILE, 'a', encoding='utf-8') as fh:
            fh.write(text + '\n')
    except Exception:
        pass


def load_cookie():
    if not os.path.isfile(COOKIE_FILE):
        log('ERROR no_cookie 缺少 %s,请从浏览器 DevTools 复制 '
            'www.workbuddy.cn 的 cookie 请求头写入该文件' % COOKIE_FILE)
        return None
    with open(COOKIE_FILE, 'r', encoding='utf-8') as fh:
        raw = fh.read().strip()
    if not raw:
        log('ERROR no_cookie cookie 文件为空')
        return None
    return raw


def call(path, cookie, method='GET', payload=None, timeout=25):
    url = BASE + path
    data = None
    headers = {
        'accept': 'application/json, text/plain, */*',
        'accept-language': 'zh-CN,zh;q=0.9',
        'cookie': cookie,
        'referer': 'https://www.workbuddy.cn/profile/growth-center',
        'user-agent': UA,
        'x-client-platform': 'web',
        'origin': 'https://www.workbuddy.cn',
    }
    if payload is not None:
        data = json.dumps(payload).encode('utf-8')
        headers['content-type'] = 'application/json'
    req = urllib.request.Request(url, data=data, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            return resp.status, resp.read().decode('utf-8', 'replace')
    except urllib.error.HTTPError as e:
        return e.code, e.read().decode('utf-8', 'replace')
    except Exception as e:
        return -1, 'EXC: %r' % (e,)


def as_json(body):
    try:
        return json.loads(body)
    except Exception:
        return None


def get_status(cookie):
    code, body = call('/status', cookie)
    if code in (401, 403):
        log('ERROR auth_expired status HTTP %s —— cookie 已失效,'
            '请重新从浏览器复制 cookie 到 wb_cookies.txt' % code)
        return None
    if code != 200:
        log('ERROR status HTTP %s body=%s' % (code, body[:200]))
        return None
    obj = as_json(body)
    if not obj or obj.get('code') != 0:
        log('ERROR status 业务码异常 %s' % body[:200])
        return None
    return obj.get('data') or {}


def do_claim(cookie, record_id):
    attempts = []
    if record_id:
        attempts.append({'record_id': record_id})
        attempts.append({'recordId': record_id})
    attempts.append({})
    for payload in attempts:
        code, body = call('/claim', cookie, method='POST', payload=payload)
        obj = as_json(body)
        if code == 200 and obj and obj.get('code') == 0:
            return True, (obj.get('data') or {}), payload
        log('  claim 尝试 payload=%s -> HTTP %s %s'
            % (list(payload.keys()) or '空body', code, body[:160]))
        if code in (401, 403):
            break
    return False, {}, None


def do_depart(cookie, loc_id):
    attempts = [
        {'location_id': loc_id},
        {'locationId': loc_id},
        {'location': loc_id},
        {'location_code': LOC_CODES.get(loc_id)},
    ]
    for payload in attempts:
        code, body = call('/depart', cookie, method='POST', payload=payload)
        obj = as_json(body)
        if code == 200 and obj and obj.get('code') == 0:
            return True, (obj.get('data') or {}), payload
        log('  depart 尝试 payload=%s -> HTTP %s %s'
            % (payload, code, body[:160]))
        if code in (401, 403):
            break
    return False, {}, None


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--dry-run', action='store_true', help='只查状态,不执行 depart/claim')
    ap.add_argument('--location', type=int, default=0,
                    help='派出地点 id(1咖啡馆 2商场 3健身房 4古镇客栈),0=随机')
    args = ap.parse_args()

    cookie = load_cookie()
    if not cookie:
        sys.exit(3)

    data = get_status(cookie)
    if data is None:
        sys.exit(4)

    state = (data.get('state') or '').lower()
    limit = bool(data.get('daily_limit_reached'))
    loc = (data.get('location') or {}).get('name') or '-'
    record_id = data.get('record_id')
    arrive_at = data.get('arrive_at')
    server_now = data.get('server_now') or int(time.time())
    reward = data.get('reward_credit')

    log('STATUS state=%s location=%s record_id=%s daily_limit=%s reward=%s'
        % (state, loc, record_id, limit, reward))

    if args.dry_run:
        log('DRY-RUN 仅查询,不执行任何写操作')
        return

    if state == 'arrived':
        ok, res, used = do_claim(cookie, record_id)
        if ok:
            got = res.get('reward_credit', reward)
            log('CLAIM 成功 +%s 积分 地点=%s payload=%s' % (got, loc, used))
        else:
            log('ERROR claim 全部尝试失败')

    elif state == 'traveling':
        remain = (arrive_at - server_now) if (arrive_at and server_now) else None
        log('SKIP 旅行中 地点=%s 剩余=%s(不重复派出)'
            % (loc, remain if remain is None else '%ds' % remain))

    elif limit:
        log('SKIP 今日已达上限(daily_limit_reached=true)')

    else:
        loc_id = args.location if args.location in LOCATIONS else random.choice(list(LOCATIONS))
        ok, res, used = do_depart(cookie, loc_id)
        if ok:
            log('DEPART 成功 地点=%s record_id=%s arrive_at=%s payload=%s'
                % (LOCATIONS[loc_id], res.get('record_id'), res.get('arrive_at'), used))
        else:
            log('ERROR depart 全部尝试失败 目标地点=%s' % LOCATIONS[loc_id])


if __name__ == '__main__':
    main()

wb_cookies.txt 的内容就是你从浏览器复制的那一行 cookie: 值,例如:

session=xxxxxxxx; session_2=yyyyyyyy; tgw_l7_route=zzzzzz

🔒 请务必替换成你自己浏览器里复制出来的真实值,且不要外发给他人。


🚀 六、跨电脑部署与定时自动化

6.1 部署包结构

把整个目录拷到目标电脑任意位置即可:

代码语言:javascript
复制
paimao-travel/
├── travel_auto.py            # 主脚本(上文代码)
├── wb_cookies.txt            # 你的 cookie(每台电脑各自生成)
├── travel_auto.log           # 运行日志(自动生成)
└── travel_state.json         # 状态快照(自动生成)

6.2 三步部署(每台电脑各做一次)

先在浏览器里找到派猫猫旅行入口(左栏成长计划 → 右栏「派猫猫旅行」):

然后按以下三步操作:

  1. 📁 放包:把目录拷到目标电脑(如 D:/paimao-travel)。
  2. 🍪 填 cookie:目标电脑 Chrome 登录 www.workbuddy.cn → 成长中心 → 派猫猫旅行;F12 → Network → 过滤 travel → 找 travel/status(HTTP 200)那条 → 复制其 Request Headers 的 cookie: 整行 → 存为同目录 wb_cookies.txt
  3. ✅ 验证 + 定时:先 python travel_auto.py --dry-run 看到 STATUS state=... 即通;再用系统任务计划 / WorkBuddy 自动化建 4 条每日定时(见下)。

6.3 四条每日定时(轮询节奏)

因为旅行要 1~4 小时,单点定时覆盖不全。用 4 个时间点轮询,确保每个状态都能被及时推进:

🕐 时间

作用

08:15

早上出门前派出(idle → depart)

12:30

午间巡检,短途可能已经 arrived → 领取

16:45

下午巡检,大部分已到 → 领取

21:00

夜间兜底巡检 / 次日状态归位

每条定时只需执行一条命令:

🛡️ 脚本自身幂等,重复跑不会出问题。

6.4 cookie 过期维护

HttpOnly session cookie 会过期(几天到十几天的会话寿命)。某天脚本报 ERROR auth_expired 或 401,只需重新复制一次 cookie 覆盖 wb_cookies.txt,代码与定时任务都无需改动。不要尝试自动登录或刷新令牌——那是另一回事。


⚠️ 七、踩坑与安全提醒(务必看)

别信 Copy as fetch:它不含 Cookie 头,必须用 Headers 面板的 cookie: 原文。 ❌ 路径别带 /v2/:否则 APISIX 网关直接 401。 ❌ 不是 JWT:这是最反直觉的一点,很多人会卡在"找不到 token"上。它就是普通 Cookie 鉴权。 🔒 Cookie = 登录凭据wb_cookies.txt 等同你的账号密码,不要外发、不要提交仓库、不要跨电脑复用。每台电脑各自生成各自的。 📋 遵守平台规则:本文仅作技术学习,请控制调用频率、勿用于刷量作弊。


🎉 八、小结

把"点一下网页"变成"定时自动跑",真正的工作量不在写代码,而在还原接口真相:错误的域名假设、被剥离的 Cookie、带 /v2/ 的 401,这三关不过,代码再漂亮也没用。

而让脚本"可跨电脑、可重复跑"的关键,是把状态权威放在服务端——脚本每次只做"读状态 → 决定一个动作"的纯函数式巡检,天然幂等。配合多方案参数回退和零依赖标准库,整个包拷到任何机器都能直接工作。

💡 希望这套"先抓包验证、再状态机落地、最后可移植封装"的思路,对你做其他网页自动化也有参考价值。

(本文所有接口结论均来自本机实测 HTTP 报文,代码已脱敏,不含任何真实凭据。)

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 🐱 用纯 HTTP 脚本自动化 WorkBuddy「派猫猫旅行」:绕过 JWT 误区、Cookie 鉴权与幂等状态机实战
    • 🎯 一、背景:为什么要写这个脚本
    • 💥 二、三个致命误区(本文核心干货)
      • ❌ 误区 1:后端在 copilot.tencent.com,鉴权用 JWT / Bearer Token
      • ❌ 误区 2:DevTools「Copy as fetch」能拿到完整请求
      • ❌ 误区 3:接口路径带 /v2/
    • 🔍 三、还原后的真实接口
    • 🧠 四、架构设计:状态机 + 幂等
      • 4.1 为什么是"轮询"而不是"一次跑完"
      • 4.2 参数名多方案回退(容错)
      • 4.3 手动验证:先跑通才有底气
      • 4.4 纯标准库、零依赖、可移植
    • 💻 五、完整可运行代码(已脱敏)
    • 🚀 六、跨电脑部署与定时自动化
      • 6.1 部署包结构
      • 6.3 四条每日定时(轮询节奏)
      • 6.4 cookie 过期维护
    • ⚠️ 七、踩坑与安全提醒(务必看)
    • 🎉 八、小结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档