feat(smzdm): 🚀 更新 App 版本并优化请求签名与风控对抗

* 升级 `APP_VERSION` 至 `11.1.95`,并同步更新 `APP_VERSION_REV`。
* 引入服务器时间同步机制:通过 `SERVER_TIME_URL` 获取服务端时间,解决本地时间偏差导致的签名失效问题。
* 优化签名算法:改进 `signFormData` 的实现,确保参数排序与空格处理符合最新 App 逻辑。
* 增强风控对抗能力:
    * 引入 `waitHuman` 模拟人类行为的等待机制,替换原有的固定 `wait`。
    * 在任务执行逻辑中加入随机打乱(Shuffle)机制,降低行为模式特征。
    * 增加对 WAF/风控拦截(如 403/429 错误或 HTML 验证码页)的识别与处理逻辑。
* 完善功能细节:
    * 支持 `SMZDM_COMMENT` 环境变量配置多条文案(使用 `|` 分隔),实现随机评论以降低风险。
    * 修复签到逻辑,支持 `CREATE_CENTER_VIEW` 和 `IS_INSTALL_ZHANGDAMA` 等新增参数。
    * 优化任务列表获取,增加对 `is_install_zhangdama` 字段的支持。
* 文档更新:在 `README.md` 中补充了新增任务类型及环境变量的使用说明。
This commit is contained in:
Hex
2026-10-13 16:56:44 +08:00
parent eff365b2a4
commit c325760c45
7 changed files with 574 additions and 286 deletions

134
bot.js
View File

@@ -3,28 +3,62 @@ const got = require('got');
// ------------------------------------
const APP_VERSION = '10.4.26';
const APP_VERSION_REV = '866';
const APP_VERSION = '11.1.95';
const APP_VERSION_REV = '1195';
const DEFAULT_USER_AGENT_APP = `smzdm_android_V${APP_VERSION} rv:${APP_VERSION_REV} (Redmi Note 3;Android10.0;zh)smzdmapp`;
const DEFAULT_USER_AGENT_WEB = `Mozilla/5.0 (Linux; Android 10.0; Redmi Build/Redmi Note 3; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/95.0.4638.74 Mobile Safari/537.36 smzdm_android_V${APP_VERSION} rv:${APP_VERSION_REV} (Redmi;Android10.0;zh) jsbv_1.0.0 webv_2.0 smzdmapp`;
const SIGN_KEY = 'apr1$AwP!wRRT$gJ/q.X24poeBInlUJC';
const ZHUANZAI_AB = '';
// 无法从设备侧还原的本地开关取值,与全新安装的 APP 行为一致
const CREATE_CENTER_VIEW = '';
const IS_INSTALL_ZHANGDAMA = '0';
// APP 用 HTTP Date 头作为签名时间,且偏差超过 5 分钟会重新获取
const SERVER_TIME_URL = 'https://app-api.smzdm.com/get_server_time';
const SERVER_TIME_MAX_AGE = 5 * 60 * 1000;
// ------------------------------------
let serverTime = 0;
let serverTimeFetchedAt = 0;
const reVersion = /(smzdm_android_V|smzdm\s|iphone_smzdmapp\/)([\d.]+)/i;
const reRev = /rv:([\d.]+)/i;
const randomStr = (len = 18) => {
const char = '0123456789';
let str = '';
// request_key = 8 位随机数 + 秒级时间戳
const requestKey = () => `${Math.floor(Math.random() * 90000000) + 10000000}${Math.floor(Date.now() / 1000)}`;
for (let i = 0; i < len; i++) {
str += char.charAt(Math.floor(Math.random() * char.length));
const getServerTime = async () => {
if (serverTime && Date.now() - serverTimeFetchedAt <= SERVER_TIME_MAX_AGE) {
return serverTime;
}
return str;
try {
// APP 直接读 HTTP Date 头,不关心状态码,取不到才回退本地时间
const response = await got(SERVER_TIME_URL, {
method: 'GET',
throwHttpErrors: false,
retry: {
limit: 0
}
});
const date = new Date(response.headers.date).getTime();
if (date) {
serverTime = date;
serverTimeFetchedAt = Date.now();
}
}
catch (error) {
console.log(`获取服务器时间失败,使用本地时间: ${error.message}`);
}
return serverTime || Date.now();
};
const parseJSON = (str) => {
@@ -39,19 +73,20 @@ const parseJSON = (str) => {
const removeTags = (str) => str.replace(/<[^<]+?>/g, '');
// 添加公共参数并签名数据
const signFormData = (data) => {
const signFormData = async (data) => {
const newData = {
weixin: 1,
basic_v: 0,
f: 'android',
v: APP_VERSION,
time: `${Math.round(new Date().getTime() / 1000)}000`,
time: `${Math.round((await getServerTime()) / 1000)}000`,
zhuanzai_ab: ZHUANZAI_AB,
...data
};
const keys = Object.keys(newData).filter(key => newData[key] !== '').sort();
const signData = keys.map(key => `${key}=${String(newData[key]).replace(/\s+/, '')}`).join('&');
const sign = crypto.createHash('md5').update(`${signData}&key=${SIGN_KEY}`).digest('hex').toUpperCase();
const signData = keys.map(key => `${key}=${newData[key]}`).join('&');
const sign = crypto.createHash('md5').update(`${signData.replace(/ /g, '')}&key=${SIGN_KEY}`).digest('hex').toUpperCase();
return {
...newData,
@@ -74,7 +109,7 @@ const requestApi = async (url, inputOptions = {}) => {
Object.keys(options.data).forEach(key => options.data[key] === undefined && delete options.data[key]);
if (options.sign !== false) {
options.data = signFormData(options.data);
options.data = await signFormData(options.data);
}
const gotOptions = {
@@ -139,10 +174,22 @@ const requestApi = async (url, inputOptions = {}) => {
})
};
const updateCookie = (cookie, name, value) => {
const re = new RegExp(`(^|;)${name}=[^;]+;`, 'ig');
// APP 每次请求都用自己维护的字段重建 Cookie,缺失的字段需要补上
const setCookie = (cookie, name, value) => {
const re = new RegExp(`(^|;)\\s*${name}=[^;]+`, 'i');
return cookie.replace(re, `$1${name}=${encodeURIComponent(value)};`);
if (re.test(cookie)) {
return cookie.replace(re, `$1${name}=${encodeURIComponent(value)}`);
}
return `${cookie.replace(/;?\s*$/, ';')} ${name}=${encodeURIComponent(value)};`;
};
// APP 不发送这些历史字段,保留会导致请求与 APP 不一致
const removeCookie = (cookie, name) => {
const re = new RegExp(`(^|;)\\s*${name}=[^;]+;?`, 'ig');
return cookie.replace(re, '$1');
};
const getEnvCookies = () => {
@@ -179,6 +226,14 @@ const wait = (minSecond, maxSecond) => {
return new Promise(resolve => setTimeout(resolve, randomSecond * 1000));
};
const waitHuman = async (minSecond, maxSecond) => {
await wait(minSecond, maxSecond);
if (Math.random() < 0.05) {
await wait(15, 45);
}
};
// ------------------------------------
class SmzdmBot {
@@ -190,16 +245,23 @@ class SmzdmBot {
// 处理 cookie
this.androidCookie = this.cookie.replace('iphone', 'android').replace('iPhone', 'Android');
this.androidCookie = updateCookie(this.androidCookie, 'smzdm_version', APP_VERSION);
this.androidCookie = updateCookie(this.androidCookie, 'device_smzdm_version', APP_VERSION);
this.androidCookie = updateCookie(this.androidCookie, 'v', APP_VERSION);
this.androidCookie = updateCookie(this.androidCookie, 'device_smzdm_version_code', APP_VERSION_REV);
this.androidCookie = updateCookie(this.androidCookie, 'device_system_version', '10.0');
this.androidCookie = updateCookie(this.androidCookie, 'apk_partner_name', 'smzdm_download');
this.androidCookie = updateCookie(this.androidCookie, 'partner_name', 'smzdm_download');
this.androidCookie = updateCookie(this.androidCookie, 'device_type', 'Android');
this.androidCookie = updateCookie(this.androidCookie, 'device_smzdm', 'android');
this.androidCookie = updateCookie(this.androidCookie, 'device_name', 'Android');
this.androidCookie = setCookie(this.androidCookie, 'v', APP_VERSION);
this.androidCookie = setCookie(this.androidCookie, 'device_smzdm_version_code', APP_VERSION_REV);
this.androidCookie = setCookie(this.androidCookie, 'device_system_version', '10.0');
this.androidCookie = setCookie(this.androidCookie, 'apk_partner_name', 'smzdm_download');
this.androidCookie = setCookie(this.androidCookie, 'partner_name', 'smzdm_download');
this.androidCookie = setCookie(this.androidCookie, 'device_smzdm', 'android');
this.androidCookie = setCookie(this.androidCookie, 'f', 'android');
this.androidCookie = setCookie(this.androidCookie, 'basic_v', '0');
[
'smzdm_version',
'device_smzdm_version',
'device_type',
'device_name'
].forEach(name => {
this.androidCookie = removeCookie(this.androidCookie, name);
});
}
getHeaders() {
@@ -215,7 +277,7 @@ class SmzdmBot {
Accept: '*/*',
'Accept-Language': 'zh-Hans-CN;q=1',
'Accept-Encoding': 'gzip',
'request_key': randomStr(18),
'request_key': requestKey(),
'User-Agent': userAgent,
Cookie: this.androidCookie
};
@@ -242,6 +304,18 @@ class SmzdmBot {
getOneByRandom(listing) {
return listing[Math.floor(Math.random() * listing.length)];
}
// 打乱顺序
getShuffled(listing) {
const result = listing.slice();
for (let i = result.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[result[i], result[j]] = [result[j], result[i]];
}
return result;
}
}
module.exports = {
@@ -250,5 +324,9 @@ module.exports = {
removeTags,
parseJSON,
getEnvCookies,
wait
getServerTime,
CREATE_CENTER_VIEW,
IS_INSTALL_ZHANGDAMA,
wait,
waitHuman
};