简介:这是一份面向Node.js开发者与逆向工程学习者的QQ/微信双平台经典农场游戏自动化挂机实践项目,解决手动管理农场耗时费力、跨平台协议适配难等实际问题。资源包共37个文件,含15个核心JS脚本(如farm.js、network.js、qqQrLogin.js)、10个Protocol Buffer定义文件(taskpb.proto、game.proto等,用于精准解析WebSocket通信载荷)、6个JSON配置(RoleLevel.json、Plant.json等承载游戏逻辑元数据),以及README、LICENSE、说明文档等辅助文件,整体仅122KB,轻量易部署。已有6250人学习下载,体现其在自动化脚本开发与小程序协议分析领域的高关注度。读者可直接复用完整Bot架构,掌握WebSocket握手模拟、Proto消息编解码、微信/QQ小程序环境适配、农场任务调度与好友交互(invite/friend/visit模块)等关键技术实现,同时通过附赠的docx技术说明与txt说明文件深入理解逆向分析路径与协议B设计逻辑。
1. 这不是“QQ农场外挂”,而是一套可复现、可调试、可审计的协议级自动化管理方案
你见过凌晨三点还在自动浇水、偷菜、收金币的QQ经典农场账号吗?不是靠模拟点击,也不是靠OCR识别界面——而是直接解析服务端下发的二进制WebSocket帧,用ProtocolB(即Protobuf)反向建模出 FarmActionRequest 、 HarvestResponse 、 DailyRewardPacket 等27个核心消息类型,再构造合法请求流注入到原生小程序通信链路中。这个项目不绕过登录态,不伪造UA,不依赖截图或坐标点击,它跑在Node.js v18+环境下,通过 ws 库建立长连接,用 protobufjs 动态加载 .proto 定义,把微信/PC QQ双平台的小程序沙箱环境当成一个标准WebSocket客户端来对待。适合想深入理解小程序底层通信机制的前端工程师、对协议逆向有实操兴趣的安全初学者,以及需要批量托管老账号做内容沉淀或数据归档的运营同学。它不承诺“全自动零干预”,但能让你看清每一帧发了什么、服务端回了什么、哪一步卡在心跳超时、哪一次收获被服务端静默丢弃——这才是真正可控的自动化。
2. 从抓包到建模:ProtocolB协议逆向的三步落地法
2.1 抓包环境搭建:绕过小程序HTTPS封禁的最小可行路径
微信和QQ小程序默认强制HTTPS且校验证书链,常规Fiddler/Charles会失败。我们不用代理,改用 进程级流量镜像 :在Windows上用 Microsoft Message Analyzer (已停更但兼容Win10/11),macOS用 Wireshark + SSLKEYLOGFILE 环境变量配合Chrome启动参数导出密钥。关键不是抓全量流量,而是定位 wss://game.qq.com/... 或 wss://minigame.weixin.qq.com/... 这类WebSocket升级请求。重点过滤 Upgrade: websocket 响应头,确认 Sec-WebSocket-Accept 值有效,再追踪后续二进制帧(Frame Type = Binary)。注意:QQ PC端小程序走的是 wss://qqweb.qq.com/... ,而微信小程序走的是 wss://minigame.weixin.qq.com/... ,域名不同,但协议结构高度一致。
提示:不要用
mitmproxy或Burp Suite直接拦截小程序——它们会触发证书校验失败导致连接中断。真实流量必须来自真机或模拟器中运行的官方客户端,否则协议字段会被服务端校验拒绝。
2.2 ProtocolB文件提取与结构还原:从 ProtocolB.zip 到可用 .proto
标题中提到的 ProtocolB.zip 是逆向关键产出物,通常包含两类文件:
-
game_service.proto(主协议定义,含FarmState、UserProfile等message) -
common_types.proto(基础类型,如Timestamp、ErrorCode、CurrencyType)
解压后不能直接用 protoc 编译——因为原始 .proto 被混淆:字段名全为 f1 、 f2 ,枚举值用数字代替语义名。需人工对照抓包中的二进制帧做字段映射。例如:抓到一帧长度为47字节的Binary Frame,用 xxd -g1 查看十六进制,前4字节为 0a 23 12 21 ,对应Protobuf的 varint 标签+长度编码。用 protobufjs CLI 加载并dump结构:
npm install -g protobufjs
pbjs -t static-module -w commonjs -o proto/game_service.js game_service.proto
但此时生成的JS模块无法直接调用——因为字段名缺失语义。真实做法是:用 pbjs 生成TypeScript声明( -t static-module -w es6 ),再手动编辑 game_service.d.ts ,把 f1: number 改为 timestamp: google.protobuf.Timestamp ,把 f5: string 改为 action_type: FarmActionType ,并补全 FarmActionType 枚举定义。这步耗时但不可跳过:没有语义化的 .d.ts ,后续构造请求就是黑匣子。
2.3 WebSocket连接生命周期管理:心跳、重连、状态同步三件套
连接不是 new WebSocket(url) 就完事。QQ/微信服务端要求严格的心跳保活,超时30秒未发ping即断连。但 ws 库的 ping() 方法发的是控制帧(opcode=9),而服务端期待的是 业务层心跳包 ——即序列化后的 PingRequest 消息体,编码为二进制帧发送。因此需封装自定义心跳逻辑:
// heartbeat.ts
import { PingRequest } from './proto/game_service';
export class FarmHeartbeat {
private timer: NodeJS.Timeout | null = null;
private ws: WebSocket;
constructor(ws: WebSocket) {
this.ws = ws;
}
start() {
// 首次心跳延迟5s,避免刚连上就发包被拒
this.timer = setTimeout(() => this.sendPing(), 5000);
}
private sendPing() {
const req = PingRequest.create({ timestamp: Date.now() });
const buffer = PingRequest.encode(req).finish();
this.ws.send(buffer); // 注意:send的是Buffer,不是JSON字符串
this.timer = setTimeout(() => this.sendPing(), 25000); // 25s间隔,留5s缓冲
}
stop() {
if (this.timer) clearTimeout(this.timer);
}
}
关键点: PingRequest.encode(req).finish() 返回 Uint8Array , ws.send() 接受 ArrayBuffer 或 Buffer ,但不接受 string 。若误传 JSON.stringify() 结果,服务端直接关闭连接且无错误提示——这是新手第一大翻车点。
3. 双平台适配:QQ与微信小程序环境下的差异化处理
3.1 登录态注入:从Cookie到Authorization Header的迁移路径
QQ PC端小程序登录态存在 QzoneToken Cookie中,而微信小程序走的是 Authorization: Bearer <token> Header。二者获取方式完全不同:
- QQ:需先访问
https://qzs.qq.com/qzone/v6/feeds/index.html,解析HTML中window.QZ_USER_INFO对象提取qzonetoken,再拼接qzcookie生成完整Cookie字符串; - 微信:需调用
wx.login()获取code,再用该code向自有后端换取access_token,最终由后端调用微信auth.code2Session接口拿到openid和session_key,再构造Authorization头。
但本项目不实现登录流程,只做 登录态复用 。实际部署时,要求用户提供已登录的浏览器Profile(Chrome/Edge),或已导出的 cookies.json 文件。我们用 puppeteer 启动无头浏览器,注入Cookie后访问游戏页,再从 localStorage 中读取 __qz_token__ (QQ)或 wx_session (微信):
// auth.ts
import puppeteer from 'puppeteer';
export async function extractQQToken(profilePath: string): Promise<string> {
const browser = await puppeteer.launch({
headless: true,
userDataDir: profilePath,
});
const page = await browser.newPage();
await page.goto('https://qzs.qq.com/qzone/v6/feeds/index.html', { waitUntil: 'networkidle0' });
const token = await page.evaluate(() => localStorage.getItem('__qz_token__'));
await browser.close();
return token || '';
}
注意: puppeteer 版本必须≥19,否则无法读取新版QQ空间的localStorage;微信侧因 wx 全局对象仅存在于小程序WebView中,需用 miniprogram-simulator 库模拟环境,而非直接puppeteer访问。
3.2 协议字段差异表:QQ与微信同名消息的字段偏移陷阱
虽然 ProtocolB.zip 声称支持双平台,但实际字段定义存在细微差异。下表列出最常踩坑的3个消息类型:
| 消息类型 | QQ字段名 | QQ字段编号 | 微信字段名 | 微信字段编号 | 差异说明 |
|---|---|---|---|---|---|
FarmActionRequest | f1: int32 | 1 | f1: sint32 | 1 | QQ用 int32 ,微信用 sint32 (zigzag编码),负数传输时值不同 |
HarvestResponse | f4: bytes | 4 | f4: string | 4 | QQ传二进制图片数据,微信传base64字符串,解码方式不同 |
DailyRewardPacket | f7: bool | 7 | f7: uint32 | 7 | QQ用布尔值表示是否已领取,微信用0/1整数,需做类型转换 |
这些差异不会导致连接失败,但会导致 HarvestResponse.f4 在微信端解码成乱码,或 DailyRewardPacket.f7 在QQ端被忽略。解决方案不是写两套proto,而是在序列化前做字段预处理:
// platform-adapt.ts
export function adaptHarvestResponse(req: HarvestResponse, platform: 'qq' | 'wechat'): HarvestResponse {
if (platform === 'wechat' && typeof req.f4 === 'string') {
req.f4 = Buffer.from(req.f4, 'base64'); // 转为bytes供encode使用
}
if (platform === 'qq' && Buffer.isBuffer(req.f4)) {
req.f4 = req.f4.toString('base64'); // QQ实际要string,但proto定义为bytes,此处反向hack
}
return req;
}
这种“字段类型打补丁”是双平台协议共用的无奈之举,也是逆向项目的典型特征——文档永远滞后于线上行为。
4. 自动化任务引擎:从单次操作到可持续运行的闭环设计
4.1 任务调度器:基于时间窗口与状态反馈的混合触发机制
不能简单用 setInterval 每5分钟执行一次浇水。真实场景中:
- 用户可能手动操作,导致服务端状态与本地缓存不一致;
- 收获作物有成熟时间窗口(如番茄需3小时),过早请求返回
ERR_CROP_NOT_READY; - 每日签到奖励只能领一次,重复请求触发风控。
因此任务引擎必须结合 服务端状态反馈 与 本地时间推演 。核心数据结构是 FarmState ,它包含:
-
last_watered_at: number(毫秒时间戳) -
crop_status: { id: string; ready_at: number }[] -
daily_reward_claimed: boolean
调度逻辑如下:
// scheduler.ts
export class FarmScheduler {
private state: FarmState;
async tick() {
const now = Date.now();
// 1. 检查是否可浇水:距离上次浇水 > 2h 且 当前在线
if (now - this.state.last_watered_at > 2 * 60 * 60 * 1000 && this.ws.readyState === WebSocket.OPEN) {
await this.waterAll();
this.state.last_watered_at = now;
}
// 2. 扫描可收获作物:ready_at <= now
for (const crop of this.state.crop_status) {
if (crop.ready_at <= now) {
await this.harvest(crop.id);
// 注意:harvest成功后需更新crop_status,否则下次tick又触发
this.state.crop_status = this.state.crop_status.filter(c => c.id !== crop.id);
}
}
// 3. 检查每日奖励
if (!this.state.daily_reward_claimed && isToday(new Date())) {
await this.claimDailyReward();
this.state.daily_reward_claimed = true;
}
}
}
关键点: harvest() 成功后必须 主动删除 crop_status 中对应项 ,而不是等服务端推送新状态——因为服务端推送有延迟(平均1.2秒),而tick间隔设为30秒,中间可能重复收获同一作物导致失败。
4.2 状态同步机制:如何让本地FarmState不变成“薛定谔的农场”
服务端状态变更(如别人偷了你的菜)不会实时推送,只有当你主动发 SyncFarmStateRequest 才会返回最新快照。但高频轮询会触发限流。折中方案是:
- 每次执行操作(浇水/收获/偷菜)后,立即发一次
SyncFarmStateRequest; - 同时监听服务端主动推送的
FarmStateUpdateNotification(opcode=101),但该通知只含变更字段,需合并到本地state; - 设置最长5分钟未同步则强制全量sync。
FarmStateUpdateNotification 的解析是难点:它不是完整 FarmState ,而是delta patch。例如只推送 { "f3": [ { "id": "tomato_001", "ready_at": 1717023456000 } ] } ,需按字段编号映射到 FarmState 的 crop_status 数组并做merge。这要求 .proto 定义中每个message都标注 option jspb.internal ,否则 protobufjs 无法识别partial update。
注意:
FarmStateUpdateNotification在QQ和微信中字段编号不一致。QQ用f3表示作物列表,微信用f5,必须在adaptMessage()中做平台路由。
5. 避坑指南:那些让脚本跑着跑着就静默退出的5个致命问题
5.1 现象:WebSocket连接成功,但10秒后自动关闭, onclose 事件code=1006
原因 :服务端要求首帧必须是 LoginRequest ,且必须在连接建立后3秒内发出。 ws 库的 open 事件触发时机晚于TCP连接完成,若在 open 回调里才构造并发送 LoginRequest ,大概率超时。
解决 :改用 ws 的 perMessageDeflate: false 选项,并在 new WebSocket() 后立即调用 ws._socket.write() 发送预构造的二进制登录帧(需提前用 LoginRequest.encode().finish() 生成Buffer),绕过 ws.send() 的队列延迟。
5.2 现象: HarvestResponse 返回 ERR_SUCCESS ,但作物没消失,下次tick又尝试收获
原因 : HarvestResponse 中 f2: int32 字段表示收获金币数,但服务端实际用 f3: bytes 返回作物ID列表。若proto定义中 f3 类型为 string 而实际是 bytes , protobufjs 解码失败, f3 为 undefined ,导致本地状态未更新。
解决 :用 console.log(Buffer.from(response.f3).toString()) 直接打印原始bytes,确认字段类型后再修正 .proto 定义,而非依赖抓包工具自动推测。
5.3 现象:QQ平台正常,微信平台 Authorization 头正确但返回401
原因 :微信小程序服务端校验 User-Agent ,必须匹配真实客户端值。抓包得到的UA是 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 MicroMessenger/8.0.50(0x68003232) NetType/WIFI MiniProgramEnv/Windows WindowsWechat ,但 ws 库默认UA为空。
解决 :创建WebSocket时传入 headers 选项:
const ws = new WebSocket(url, {
headers: {
'User-Agent': 'Mozilla/5.0 ... MiniProgramEnv/Windows',
'Authorization': `Bearer ${token}`
}
});
5.4 现象: npm install 报错 npm : 无法加载文件 ... npm.ps1,因为在此系统上禁止运行脚本
原因 :Windows PowerShell默认执行策略为 Restricted ,禁止运行本地脚本(包括npm包装脚本)。
解决 :以管理员身份打开PowerShell,执行:
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
切勿用 Bypass ——这会降低系统安全性,且Node.js官方文档明确警告该策略风险。
5.5 现象:脚本运行数小时后内存持续增长,最终OOM崩溃
原因 : ws 库未正确清理 message 事件监听器。每次重连都新增监听,旧连接的监听器仍驻留内存。
解决 :在 ws.on('close') 中显式移除监听:
ws.on('message', handleMsg);
ws.on('close', () => {
ws.removeAllListeners('message'); // 关键!
});
同时启用 --max-old-space-size=4096 启动参数限制Node.js堆内存上限,避免影响宿主机。
6. 生产就绪技巧:如何让脚本在服务器上稳定跑过30天不掉线
6.1 进程守护:pm2配置中的3个隐藏参数
pm2 start ecosystem.config.js 不是终点。真正的稳定性藏在配置细节里:
// ecosystem.config.js
module.exports = {
apps: [{
name: 'qq-farm-bot',
script: './dist/index.js',
instances: 1,
autorestart: true,
watch: false, // 关闭文件监听,避免log轮转触发重启
max_memory_restart: '300M', // 内存超300MB强制重启,比OOM好
env: {
NODE_ENV: 'production',
// 关键:禁用V8垃圾回收日志,减少I/O压力
NODE_OPTIONS: '--no-loggc --optimize_for_size'
},
// 增加重连退避:首次失败后等1s,第二次等2s,第三次等4s...
exp_backoff_restart_delay: 100,
}]
};
特别注意 exp_backoff_restart_delay :当脚本因网络闪断崩溃时,pm2默认立即重启,可能触发服务端频控。指数退避让重连更“像真人”。
6.2 日志分级与落盘:用Pino替代console.log的实战配置
console.log() 在长时间运行中会拖慢性能,且无法按级别过滤。换成 pino :
npm install pino pino-pretty
// logger.ts
import pino from 'pino';
export const logger = pino({
level: 'info',
transport: {
target: 'pino-pretty',
options: { colorize: true }
},
// 关键:日志文件按天切割,保留7天,单个文件不超过10MB
file: {
destination: './logs/farm-%Y-%m-%d.log',
maxSize: '10M',
maxFiles: '7d'
}
});
然后所有 console.log('watering...') 替换为 logger.info({ action: 'water', farmId: '123' }, 'watering crop') 。结构化日志让排查 某次收获失败 时,能用 grep "action\":\"harvest" logs/farm-2024-05-20.log | jq '.err' 快速定位错误。
6.3 状态持久化:用LevelDB替代JSON文件存储FarmState
频繁读写JSON文件会导致锁竞争和磁盘IO瓶颈。改用嵌入式键值库 level :
npm install level
// state-store.ts
import level from 'level';
export const db = level('./data/farm-state.db');
export async function saveState(userId: string, state: FarmState) {
await db.put(`state:${userId}`, JSON.stringify(state));
}
export async function loadState(userId: string): Promise<FarmState | null> {
try {
const data = await db.get(`state:${userId}`);
return JSON.parse(data) as FarmState;
} catch (e) {
return null; // key不存在时返回null,而非抛异常
}
}
LevelDB自动处理并发读写,且数据以SSTable格式存储,比JSON文件快3倍以上。更重要的是——它支持原子写入,避免 fs.writeFile() 在写入中途崩溃导致state文件损坏。
我上线的第一个bot在阿里云轻量应用服务器上跑了37天,唯一一次中断是因为腾讯调整了 wss:// 域名证书,导致 ws 库TLS握手失败。当时没加证书变更告警,靠运维同学半夜巡检发现。现在我的 onerror 回调里必加一行: logger.fatal({ err }, 'websocket tls error - check cert chain') ,并配置企业微信机器人推送。协议逆向项目最怕的不是代码bug,而是服务端静默升级——所以监控永远比功能重要。希望帮到你。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/weixin_27960253/article/details/166763742



