
💡 一句话导语:把网页端每日积分小游戏改造成可定时、可跨电脑运行的纯 Python 脚本。重点不在"拿到积分",而在三个真实踩坑和一套状态机 + 幂等设计。所有结论均来自本机实测 HTTP 报文,文末附完整可运行代码(已脱敏)。
WorkBuddy 网页端的「成长计划 → 派猫猫旅行」里有个小猫旅行玩法:每天把小猫派去 4 个地点之一(☕ 咖啡馆 / 🛍️ 商场店铺 / 💪 健身房 / 🏮 古镇客栈),旅行 1—4 小时后回来,领取 5—10 积分,每日限一次。下图是实际玩法界面:

玩法简单,但手动操作有两个痛点:
🎯 目标:写个脚本,定时自动"派出 → 等待 → 领取",并且能直接拷到别的电脑上跑。
动手前,我凭经验做了三个假设,结果每一个都是错的,逐一实测纠正。
copilot.tencent.com,鉴权用 JWT / Bearer Token第一反应是"这是腾讯的 AI 产品,后端在 copilot.tencent.com,请求头里一定有 Authorization: Bearer <token>"。
✅ 实测打脸:用 Chrome 登录后抓包,所有
travel/*请求的真实域名是www.workbuddy.cn,而且请求头里根本没有Authorization。
这是最隐蔽的坑。在 Network 面板右键请求 → Copy as fetch,生成的 fetch() 代码里 Cookie 头被 Chrome 主动剥离了(浏览器出于安全考虑,不会在复制出的代码里暴露 Cookie)。
⚠️ 如果只信
Copy as fetch,你会得到一个"永远 401"的请求。 正确做法是:直接复制 Request Headers 面板里cookie:这一行的原始文本,而不是复制 fetch 代码。
/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 为权威状态 |
四个端点:
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 | 服务端当前时间戳(用于算剩余时间) |
旅行要 1~4 小时,不可能在一个进程里 sleep 几小时。所以设计成:多个定时时刻去"巡检"一次,每次根据服务端真实状态决定动作。脚本本身无状态,服务端 data.state 是唯一权威。
状态机逻辑(幂等都是靠服务端状态保证的):
┌──────────────────────────────────────────────────────────┐
│ GET /status → 读 data.state │
│ │
│ state == arrived → POST /claim (领积分,只一次) │
│ state == traveling → 跳过 (绝不重复派) │
│ daily_limit_reached → 跳过 (今日已领满) │
│ 其他(idle) → POST /depart (随机派一地) │
└──────────────────────────────────────────────────────────┘🛡️ 这套设计天然幂等:即使定时器抖动、脚本重复执行,也不会重复派或重复领——因为动作完全由服务端状态驱动。

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



💡 这一步的意义:证明(a)接口设计跑得通、(b)真实到账不只是前端显示、(c)积分涨幅在预期区间内。自动化只是把这个手动过程变成可循环的脚本。
脚本只用 urllib(Python 标准库),不依赖 requests 等第三方包,Python 3.8+ 任意环境可跑。所有路径基于 __file__ 相对计算,不写死任何绝对路径,因此整个目录拷到别的电脑就能跑。
凭据放在同目录 wb_cookies.txt,内容为从浏览器复制的 cookie: 原始值(见下文部署说明)。脚本完整如下:
# -*- 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
🔒 请务必替换成你自己浏览器里复制出来的真实值,且不要外发给他人。
把整个目录拷到目标电脑任意位置即可:
paimao-travel/
├── travel_auto.py # 主脚本(上文代码)
├── wb_cookies.txt # 你的 cookie(每台电脑各自生成)
├── travel_auto.log # 运行日志(自动生成)
└── travel_state.json # 状态快照(自动生成)6.2 三步部署(每台电脑各做一次)
先在浏览器里找到派猫猫旅行入口(左栏成长计划 → 右栏「派猫猫旅行」):

然后按以下三步操作:
D:/paimao-travel)。www.workbuddy.cn → 成长中心 → 派猫猫旅行;F12 → Network → 过滤 travel → 找 travel/status(HTTP 200)那条 → 复制其 Request Headers 的 cookie: 整行 → 存为同目录 wb_cookies.txt。python travel_auto.py --dry-run 看到 STATUS state=... 即通;再用系统任务计划 / WorkBuddy 自动化建 4 条每日定时(见下)。因为旅行要 1~4 小时,单点定时覆盖不全。用 4 个时间点轮询,确保每个状态都能被及时推进:
🕐 时间 | 作用 |
|---|---|
08:15 | 早上出门前派出(idle → depart) |
12:30 | 午间巡检,短途可能已经 arrived → 领取 |
16:45 | 下午巡检,大部分已到 → 领取 |
21:00 | 夜间兜底巡检 / 次日状态归位 |
每条定时只需执行一条命令:
🛡️ 脚本自身幂等,重复跑不会出问题。

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 删除。