feat(smzdm): 🚀 优化风控对抗逻辑并完善签到/任务流程

* **风控对抗增强**:
    * 更新 `APP_VERSION` 至 `11.1.98`。
    * 引入 `device_id` 确定性派生机制,通过 `userId` 结合 `DEVICE_SALT` 生成固定的设备串,解决跨运行设备不一致问题。
    * 优化 `sk` (设备签名) 生成逻辑,支持基于派生设备串自动计算。
    * 增加 `Cookie` 完整性检查,自动检测并提示缺失 `device_id`、`device_rid`、`z_dr` 等关键字段。
* **签到流程优化**:
    * 引入 `getCheckinCard` 接口,先渲染签到卡片以获取最新的连签状态,确保奖励判断准确。
    * 移除硬编码的 `IS_INSTALL_ZHANGDAMA` 参数。
* **任务处理优化**:
    * 增加 `getCaptchaType` 逻辑,在领取任务失败时识别验证码类型(滑块/图形),并友好提示用户手动操作。
    * 完善 `getArticleDetail` 接口,支持传递 `hashcode` 以匹配 APP 端跳转逻辑。
* **文档更新**:
    * 更新 `README.md`,详细说明 `SMZDM_SK` 的使用逻辑、设备字段缺失的影响以及如何正确抓包。
This commit is contained in:
Hex
2026-10-22 01:20:56 +08:00
parent 9aa6d3dbae
commit 0b71d792b8
5 changed files with 273 additions and 59 deletions

View File

@@ -5,6 +5,7 @@ smzdm 签到脚本
cron: 10 8 * * *
*/
const crypto = require('crypto');
const Env = require('./env');
const {
SmzdmBot,
@@ -13,8 +14,7 @@ const {
getEnvCookies,
wait,
waitHuman,
CREATE_CENTER_VIEW,
IS_INSTALL_ZHANGDAMA
CREATE_CENTER_VIEW
} = require('./bot');
const notify = require('./sendNotify');
const CryptoJS = require('crypto-js');
@@ -31,6 +31,9 @@ class SmzdmCheckinBot extends SmzdmBot {
}
async run() {
// 客户端先渲染签到卡片,用户点签到后才发 /checkin
await this.getCheckinCard();
const { msg: msg1 } = await this.checkin();
const { msg: msg2 } = await this.allReward();
@@ -46,7 +49,7 @@ class SmzdmCheckinBot extends SmzdmBot {
headers: this.getHeaders(),
data: {
touchstone_event: '',
sk: this.sk || '1',
sk: this.sk,
token: this.token,
captcha: ''
}
@@ -127,7 +130,8 @@ class SmzdmCheckinBot extends SmzdmBot {
}
async extraReward() {
const isContinue = await this.isContinueCheckin();
// 连签奖励状态以签到后的卡片为准,所以这里重新拉一次
const isContinue = this.isContinueCheckin(await this.getCheckinCard());
if (!isContinue) {
const msg = '今天没有额外奖励';
@@ -167,26 +171,31 @@ class SmzdmCheckinBot extends SmzdmBot {
}
}
async isContinueCheckin() {
// 签到卡片,APP 进签到页时先渲染它
async getCheckinCard() {
const { isSuccess, data, response } = await requestApi('https://user-api.smzdm.com/checkin/show_view_v2', {
method: 'post',
headers: this.getHeaders(),
data: {
is_install_zhangdama: IS_INSTALL_ZHANGDAMA
}
headers: this.getHeaders()
});
if (isSuccess) {
// 连续签到的卡片,直接按有没有 checkin_continue 来找
const result = data.data.rows.find(item => item.cell_data && item.cell_data.checkin_continue);
return !!(result && result.cell_data.checkin_continue.continue_checkin_reward_show);
return data.data;
}
else {
$.log(`查询是否有额外奖励失败!${response}`);
$.log(`查询签到卡片失败!${response}`);
return false;
}
// 连续签到的卡片,直接按有没有 checkin_continue 来找
isContinueCheckin(card) {
if (!card || !card.rows) {
return false;
}
const result = card.rows.find(item => item.cell_data && item.cell_data.checkin_continue);
return !!(result && result.cell_data.checkin_continue.continue_checkin_reward_show);
}
async getVipInfo() {
@@ -210,27 +219,44 @@ class SmzdmCheckinBot extends SmzdmBot {
}
}
function random32() {
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
const SK_KEY = 'geZm53XAspb02exN';
const warned = new Set();
function warnOnce(key, message) {
if (warned.has(key)) {
return;
}
warned.add(key);
$.log(message);
}
function getUserId(cookie) {
const match = cookie.match(/smzdm_id=([^;]*)/);
return match ? match[1] : '';
}
const DEVICE_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
// 盐值一旦定下就不能改,改了等于所有账号换设备
const DEVICE_SALT = 'smzdm_script_device_v1';
// APP 的设备串是 62 字符集 32 位随机串,这里按账号确定性派生,保证跨运行固定不变
function makeDeviceId(userId) {
const digest = crypto.createHash('sha256').update(`${DEVICE_SALT}:${userId}`).digest();
let result = '';
for (let i = 0; i < 32; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
result += DEVICE_ALPHABET.charAt(digest[i] % DEVICE_ALPHABET.length);
}
return result;
}
function getSk(cookie) {
const matchUserId = cookie.match(/smzdm_id=([^;]*)/);
if (!matchUserId) {
return ''
}
const userId = matchUserId[1];
const deviceId = getDeviceId(cookie);
const key = CryptoJS.enc.Utf8.parse('geZm53XAspb02exN');
function makeSk(userId, deviceId) {
const key = CryptoJS.enc.Utf8.parse(SK_KEY);
const cipherText = CryptoJS.DES.encrypt(userId + deviceId, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
@@ -239,14 +265,81 @@ function getSk(cookie) {
return cipherText.toString();
}
function getDeviceId(cookie) {
const matchDeviceId = cookie.match(/device_id=([^;]*)/);
if (matchDeviceId) {
return matchDeviceId[1];
// sk 是 DES(userId + 设备 id) 的 Base64,可逆;返回设备段,不属于这个账号就返回空串
function decodeSkDevice(sk, userId) {
if (!userId) {
return '';
}
return random32();
try {
const plain = CryptoJS.DES.decrypt(
{ ciphertext: CryptoJS.enc.Base64.parse(sk) },
CryptoJS.enc.Utf8.parse(SK_KEY),
{
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}
).toString(CryptoJS.enc.Utf8);
return plain.indexOf(userId) === 0 ? plain.slice(userId.length) : '';
}
catch (e) {
return '';
}
}
// cookie 缺 smzdm_id 时,/vip 的 data.base.user_smzdm_id 还能拿到(APP 也用它回写 sp,sk 的 userId 正是来自这里)
async function fetchUserId(cookie) {
const bot = new SmzdmBot(cookie);
const { isSuccess, data } = await requestApi('https://user-api.smzdm.com/vip', {
method: 'post',
headers: bot.getHeaders(),
data: {
token: bot.token,
create_center_view: CREATE_CENTER_VIEW
}
});
// smzdm_id 在响应信封顶层(每个响应都有),base.user_smzdm_id 作为兜底
const userId = isSuccess
? (data.smzdm_id || (data.data && data.data.base ? data.data.base.user_smzdm_id : ''))
: '';
return userId && userId != '0' ? String(userId) : '';
}
const md5 = (str) => crypto.createHash('md5').update(str).digest('hex');
function getCookieValue(cookie, name) {
const match = cookie.match(new RegExp(`(?:^|;)\\s*${name}=([^;]*)`, 'i'));
return match ? match[1] : '';
}
// 覆盖或补上 cookie 字段,值为空也要能覆盖
function upsertCookie(cookie, name, value) {
const re = new RegExp(`(^|;)\\s*${name}=[^;]*;?`, 'i');
if (re.test(cookie)) {
return cookie.replace(re, `$1${name}=${value};`);
}
return `${cookie.replace(/;?\s*$/, ';')} ${name}=${value};`;
}
// APP 的 device_rid/device_s 就是 md5(设备串),把 cookie 对齐到 sk 实际用的设备段
function alignDeviceCookie(cookie, deviceId) {
const hash = md5(deviceId);
let result = cookie;
if (getCookieValue(result, 'device_rid') !== hash) {
result = upsertCookie(result, 'device_rid', hash);
}
if (getCookieValue(result, 'device_s') !== hash) {
result = upsertCookie(result, 'device_s', hash);
}
return result;
}
!(async () => {
@@ -275,15 +368,64 @@ function getDeviceId(cookie) {
let notifyContent = '';
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i];
let cookie = cookies[i];
if (!cookie) {
continue;
}
let sk = sks[i];
if (!sk) {
sk = getSk(cookie)
let userId = getUserId(cookie);
let sk = (sks[i] || '').trim();
let deviceId = '';
if (!userId) {
// 少数 Cookie 只有 sess 没 smzdm_id,用 /vip 把它补回来,避免退化成 sk=1
userId = await fetchUserId(cookie);
if (userId) {
warnOnce('vip-id', 'Cookie 里没有 smzdm_id,已从 /vip 接口取到');
}
}
if (sk) {
// 用配置的真 sk,并把 cookie 对齐到它自己的设备段(同机抓的两个值本来就相等)
deviceId = decodeSkDevice(sk, userId);
if (!deviceId) {
$.log(`🟡第 ${i + 1} 个 SMZDM_SK 解不出设备段(不是有效的 sk 或不属于这个账号),签到很可能失败,请检查多账号顺序`);
}
}
else if (userId) {
// 没有真值就按账号派生一个固定设备串,sk 和 cookie 用同一个
deviceId = makeDeviceId(userId);
sk = makeSk(userId, deviceId);
warnOnce(
'auto',
'未配置 SMZDM_SK,改用按账号派生的固定设备串生成 sk;如需与真机完全一致,请在 APP 的签到请求里抓 sk 填入 SMZDM_SK'
);
}
else {
// cookie 和 /vip 都拿不到 userId,sk 拼不出来;发 1 是最后一档,至少保持参数存在
sk = '1';
warnOnce(
'empty',
'Cookie 里没有 smzdm_id,/vip 也没取到,只能发 sk=1;这种 Cookie 很可能被判定异常,建议重新从 APP 抓包'
);
}
if (deviceId) {
const aligned = alignDeviceCookie(cookie, deviceId);
if (aligned !== cookie) {
warnOnce(
'align',
'cookie 的 device_rid/device_s 与 sk 的设备段不一致,已按 sk 对齐(cookie 和 sk 可能不是同一台设备)'
);
}
cookie = aligned;
}
if (i > 0) {