Files
smallfawnandClaude Opus 5 f8080f1fa7 fix(wxapp): wx_xlxyh 签到/抽奖补前置点击埋点,两个恒失败的接口都通了
真因不在 signIn 这个请求里:服务端要求先收到一条「你点了这个按钮」的埋点
事件,之后那个动作才放行。

  POST /api/buryPointApp/save
    userId, openId, activitySource=Xcx_MeiRiRenWu,
    urlPath=pages/task-center/index, urlName=任务中心,
    elementName=每日签到, elementType=页面, eventNameEn=MPClick
  → 等 0.4~1.1s → GET /api/user/signIn?userId=…

同一把会话、同一分钟的对照实验:
  不打埋点 → signIn 恒回 40001「登录过期」
  打了埋点 → signIn 回 200 {state:3,coreCoin:10},签到成功、连续 1->2 天、
             芯动值 165 -> 175
抽奖同理(换转盘页埋点 pages/wheel/index + elementName=立即抽奖):
  不打埋点 → getLuck 回 code=1「非法请求」,连只读的 luckDraw/list 也回 code=1
  打了埋点 → getLuck 回 200 并中「5芯动值」
实跑复核:每日任务列表里 ✔每日签到 ✔每日参与抽奖 ✔每日点赞文章。

这条线索来自 GitHub 上 lcmovie/YYB-GO-Script-i 与 cm1916/YB-GO-Script-i 的
骁龙骁友会.js(preheatDailySign),据此定向验证后落地。

同时改正文件头里两条错误结论:40001 不是「服务端单独挡住这一路」,
code=1 也不是「通用错误兜底」,两者都只是缺前置埋点。
fixture 三场景 15/15/13 条断言全通过(含埋点必须在 signIn/getLuck 之前的顺序断言)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 12:13:35 +08:00

674 lines
29 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
------------------------------------------
@Author: sm
@Date: 2026.08.16
@Description: 骁龙骁友会 小程序 签到 + 每日任务(抽奖/点赞/阅读5分钟/VLOG1分钟)
cron: 20 9 * * *
------------------------------------------
#Notice:
变量名:wx_xlxyh
变量值:wx_server 里的 openid/账号标识,多账户用 & 或换行;支持 openid#备注
也兼容旧格式 sessionKey#userId(手填会话,过期就得重填,不推荐)
需要配置:wx_server_url、wx_auth
可选变量:
wx_xlxyh_read_task 是否做两个「停留时长」任务,默认 1(开启)。
阅读文章要真等 5 分钟、VLOG 要真等 1 分钟——服务端是用
enterReadDaily / exitReadDaily 两次调用的时间差算时长的,
没有任何时长参数可以传,所以只能真等。整个脚本因此约跑 6.5 分钟。
不想让任务跑这么久就置 0,只做签到/抽奖/点赞。
登录链路(反编译主包 wx026c06df6adc5d06 逐行核对,A21B5B03…js):
wx.login code -> POST /api/user/getOpenId {code}
-> {openId, sessionKey, userInfo};userInfo.id 就是 userId
之后每个请求把 userId / sessionKey / openId 放在【请求头】上(22E9D2A0…js)
userInfo.id === 0 表示这个微信号还没在骁友会注册过,脚本会直接提示而不是瞎跑。
请求签名(22E9D2A0…js + B1596B97…js:165/284 + 563700E2…js):
sign = md5( joinJson(参数) + requestId + timestamp ) —— 小写、无盐
joinJson: k1=encodeURIComponent(v1)&k2=... (对象自身的枚举顺序)
requestId: 源码 uuid() 无参调用,第 8/13/18/23 位被置成 undefined 再 join,
实际得到的是 32 位十六进制而不是标准 UUID,必须照抄这个 quirk。
注意:必须带微信小程序 User-Agent,否则 getOpenId 直接回 {"code":1,"message":"非法请求"}。
另注:sign 服务端不校验(故意写错签名,signList / checkActivity 照样回 200),
真正的准入是那个 User-Agent;照抄签名只是为了跟真机一致。
(sign 的算法本身是核对过的:563700E2…js 的 hex_md5 就是标准小写 md5,
h() 按 charCodeAt&255 取字节,l() 输出小写 hex,没有任何私货。)
签到 / 抽奖的【前置条件】:先打一次点击埋点(2026-08-18 实测定性)
这个后端要求先收到一条"你点了这个按钮"的埋点事件,之后那个动作才放行:
POST /api/buryPointApp/save
userId, openId, activitySource=Xcx_MeiRiRenWu,
urlPath=pages/task-center/index, urlName=任务中心,
elementName=每日签到, elementType=页面, eventNameEn=MPClick
→ 等 0.4~1.1 秒 → GET /api/user/signIn?userId=…
对照实验(同一把会话、同一分钟):
不打埋点 → signIn 恒回 40001「登录过期,请重新登录」
打了埋点 → signIn 回 200 {state:3, coreCoin:10},签到成功、连续天数 +1
抽奖同理,换成转盘页的埋点(urlPath=pages/wheel/index、elementName=立即抽奖):
不打埋点 → getLuck 回 code=1「非法请求」,连只读的 luckDraw/list 都回 code=1
打了埋点 → getLuck 回 200 并中奖
真机每次点按钮都会先发埋点,所以线上永远撞不到;脚本不发就一直被拒。
⚠️ 这也是为什么之前两天 15 轮变量法(换会话/时间戳/requestId/UA/Referer/GET-POST/
签名大小写/换别人 userId)全都无效 —— 门槛根本不在 signIn 这个请求里面。
点赞 / 阅读 5 分钟 / VLOG 1 分钟 三个任务不需要埋点前置,直接就能过。
⚠️【免责声明】
------------------------------------------
1、此脚本仅用于学习研究,不保证其合法性、准确性、有效性,请根据情况自行判断,本人对此不承担任何保证责任。
2、由于此脚本仅用于学习研究,您必须在下载后 24 小时内将所有内容从您的计算机或手机或任何存储设备中完全删除,若违反规定引起任何事件本人对此均不负责。
3、请勿将此脚本用于任何商业或非法目的,若违反规定请自行对此负责。
4、此脚本涉及应用与本人无关,本人对因此引起的任何隐私泄漏或其他后果不承担任何责任。
5、本人对任何脚本引发的问题概不负责,包括但不限于由脚本错误引起的任何损失和损害。
6、如果任何单位或个人认为此脚本可能涉嫌侵犯其权利,应及时通知并提供身份证明,所有权证明,我们将在收到认证文件确认后删除此脚本。
7、所有直接或间接使用、查看此脚本的人均应该仔细阅读此声明。本人保留随时更改或补充此声明的权利。一旦您使用或复制了此脚本,即视为您已接受此免责声明。
*/
const { Env } = require("../tools/env.js");
const $ = new Env("骁龙骁友会");
const axios = require("axios");
const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
const WeChatServer = require("../wxapp/wcs.js");
const ckName = "wx_xlxyh";
const strSplitor = "#";
const MINI_APP_ID = "wx026c06df6adc5d06";
const PAGE_VERSION = "644";
// BC195B25ACE3C9CFDA7F33222D124737.js: API_ROOT(release)
const API_BASE = "https://qualcomm.boysup.cn/qualcomm-app";
const TOKEN_CACHE_FILE = path.join(__dirname, "wx_xlxyh_token_cache.json");
const WX_SERVER_URL = process.env.wx_server_url || "";
// 两个停留时长任务开关:服务端按 enter/exit 两次调用的时间差算时长,只能真等
const READ_TASK = !/^(0|false|no|off)$/i.test(String(process.env.wx_xlxyh_read_task ?? "1"));
const defaultUserAgent =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090a13) UnifiedPCWindowsWechat(0xf254162e) XWEB/18151";
// 端点(B3F98543ACE3C9CFD59FED444E024737.js / 671C83A1…js,方法与参数逐条核对)
const EP_GET_OPENID = "/api/user/getOpenId"; // POST {code} postNoSession
const EP_USER_INFO = "/api/user/info"; // GET {userId}
const EP_SIGN_LIST = "/api/user/signList"; // GET {userId}
const EP_SIGN_IN = "/api/user/signIn"; // GET {userId} ← 是 GET,不是 POST
const EP_TASK_DAILY = "/api/home/taskDaily"; // GET {userId}
const EP_DRAW_LIST = "/api/luckDraw/list"; // GET {page,userId,activityId}
const EP_GET_LUCK = "/api/luckDraw/getLuck"; // POST {userId,activityId}
const EP_BURY_POINT = "/api/buryPointApp/save"; // POST 点击埋点,签到/抽奖的前置
const EP_ARTICLES = "/api/home/articles"; // GET {page,size,userId,type,searchDate,articleShowPlace}
const EP_ARTICLE_LIKE = "/api/article/like"; // GET {articleId,userId}
const EP_ENTER_READ = "/api/article/enterReadDaily"; // POST {articleId,userId}
const EP_EXIT_READ = "/api/article/exitReadDaily"; // POST {articleId,userId}
const EP_VLOG_LIST = "/api/article/vlogList"; // GET {page,size,userId,sortBy}
const EP_VLOG_PLAY = "/api/article/vlogPlay"; // GET {articleId}
const EP_SYS_CONFIG = "/api/sysConfig/detail"; // POST {propertyKey}
const EP_QUIZ_DETAIL = "/api/interactQuestion/detail"; // POST {userId}
// pages/wheel/index.js:34 —— 客户端自己也是硬编码 7,不存在活动 id 轮换
const LUCK_ACTIVITY_ID = 7;
// pages/article-details/index.js:299 —— 5 == minute 才算完成;userExitRead 在 lookTimes>=300 时直接 return
const READ_SECONDS = 300;
// pages/vlog/detail.js:197 —— 1 == minute 才算完成;进入条件是 lookTimes < 60
const VLOG_SECONDS = 60;
const VLOG_SWITCH_KEY = "task_switch_DAILY_PLAY_VIDEO_1_MINUTES";
// 22E9D2A0…js: f = String(code),放行 "200" 与 "40003";40001 = 会话失效需重登
const SUCCESS_CODES = new Set(["200", "40003"]);
const CODE_SESSION_EXPIRED = "40001";
const wechat = new WeChatServer({
url: WX_SERVER_URL || "http://192.168.31.196:8787",
appid: MINI_APP_ID,
auth: process.env.wx_auth || "",
});
function readTokenCache() {
try {
if (!fs.existsSync(TOKEN_CACHE_FILE)) return {};
return JSON.parse(fs.readFileSync(TOKEN_CACHE_FILE, "utf8")) || {};
} catch (e) {
return {};
}
}
function writeTokenCache(cache) {
try {
fs.writeFileSync(TOKEN_CACHE_FILE, JSON.stringify(cache, null, 2), "utf8");
} catch (e) {
$.log(`写入token缓存失败: ${e.message || e}`);
}
}
function md5(str) {
return crypto.createHash("md5").update(str).digest("hex");
}
// B1596B97ACE3C9CFD73F039088324737.js:165 exports.joinJson
function joinJson(data) {
let out = "";
let i = 0;
for (const key in data || {}) {
if (i > 0) out += "&";
out += `${key}=${encodeURIComponent(data[key])}`;
i++;
}
return out;
}
// B1596B97ACE3C9CFD73F039088324737.js:284 exports.uuid
// 源码调用处是无参 uuid(),于是 t[8]/t[13]/t[18]/t[23] 被赋成 undefined,
// join("") 会把 undefined 丢掉 —— 实际发出去的是 32 位十六进制。照抄。
function requestId() {
const t = [];
for (let i = 0; i < 36; i++) t[i] = "0123456789abcdef".substr(Math.floor(16 * Math.random()), 1);
t[14] = "4";
t[19] = "0123456789abcdef".substr((3 & parseInt(t[19], 16)) | 8, 1);
t[8] = t[13] = t[18] = t[23] = undefined;
return t.join("");
}
function bizCode(result) {
return String(result?.code ?? "");
}
function isSuccess(result) {
return SUCCESS_CODES.has(bizCode(result));
}
// 幂等:重复签到/重复完成时服务端文案不止一种
function isAlreadyDone(message) {
return /已签|已经签|签到过|重复|已完成|已领|already/i.test(String(message || ""));
}
function maskPhone(phone = "") {
return String(phone).replace(/^(\d{3})\d{4}(\d{4})$/, "$1****$2");
}
function shortId(value = "") {
const s = String(value || "");
return s ? `${s.slice(0, 4)}***${s.slice(-4)}` : "";
}
function stripHtml(text = "") {
return String(text || "")
.replace(/<[^>]*>/g, "")
.trim();
}
function cut(text = "", n = 26) {
const s = String(text || "").replace(/\s+/g, " ").trim();
return s.length > n ? `${s.slice(0, n)}…` : s;
}
class Task {
constructor(env) {
this.index = $.userIdx++;
this.raw = String(env || "").trim();
this.user = this.raw.split(strSplitor);
this.accountId = (this.user[0] || "").trim();
this.remark = (this.user[1] || "").trim();
this.userId = 0;
this.sessionKey = "";
this.openId = "";
this.userInfo = {};
this.cache = readTokenCache();
// 旧格式 sessionKey#userId:第一段是 base64 会话而不是 openid,第二段是纯数字
this.legacySession = /^\d+$/.test(this.remark) && /[=+/]/.test(this.accountId);
if (this.legacySession) {
this.sessionKey = this.accountId;
this.userId = Number(this.remark);
this.accountId = "";
}
}
get tag() {
const who = this.userInfo.nick || this.remark || shortId(this.accountId) || `userId ${this.userId}`;
return `账号[${this.index}] ${who}`;
}
log(msg) {
$.log(`${this.tag}: ${msg}`);
}
/**
* 22E9D2A0…js:u()/i() —— 参数一律走 joinJson,GET 拼 query、POST 放 body,
* 会话三件套(userId/sessionKey/openId)和签名都在请求头上。
* withSession=false 对应 postNoSession(userId "0"、sessionKey/openId 空串)。
*/
async request(endpoint, { method = "GET", data = null, withSession = true, retry = true } = {}) {
const body = joinJson(data);
const ts = Date.now();
const rid = requestId();
const headers = {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
userId: withSession ? String(this.userId || 0) : "0",
sessionKey: withSession ? this.sessionKey || "" : "",
openId: withSession ? this.openId || "" : "",
timestamp: String(ts),
requestId: rid,
sign: md5(body + rid + ts),
"User-Agent": defaultUserAgent,
Referer: `https://servicewechat.com/${MINI_APP_ID}/${PAGE_VERSION}/page-frame.html`,
Accept: "*/*",
xweb_xhr: "1",
};
const config = {
url: API_BASE + endpoint,
method,
headers,
timeout: 30000,
validateStatus: () => true,
};
if (method === "GET") {
if (body) config.url += `?${body}`;
} else {
config.data = body;
}
let result;
try {
const res = await axios(config);
result = typeof res.data === "string" ? JSON.parse(res.data) : res.data;
} catch (e) {
return { code: "-1", message: e.message || "网络错误" };
}
// 22E9D2A0…js: 40001 -> clearUserInfo + refreshUserInfo + 重放一次
if (bizCode(result) === CODE_SESSION_EXPIRED && retry && !this.legacySession) {
this.log("会话失效,重新登录后重试");
if (await this.login(true)) {
return this.request(endpoint, { method, data, withSession, retry: false });
}
}
return result || {};
}
/**
* 从 wx_server 取一次性 code。
* wcs.getCode 在 status:false 时也会 resolve,必须自己判失败,
* 否则 smallcat 的取码限流会被误当成骁友会登录失败。
*/
async getServerCode() {
if (!process.env.wx_auth) throw new Error("缺少 wx_auth,无法从 wx_server 取 code");
const { data } = await wechat.getCode(this.accountId);
if (data?.status === false) throw new Error(`wx_server 取码失败: ${data?.message || data?.error || "未知原因"}`);
const code = data?.code || data?.data?.code;
if (!code) throw new Error("wx_server 未返回 code");
return code;
}
/** A21B5B03…js: wx.login code -> getOpenId -> {openId, sessionKey, userInfo} */
async loginByWxCode() {
const code = await this.getServerCode();
const result = await this.request(EP_GET_OPENID, {
method: "POST",
data: { code },
withSession: false,
retry: false,
});
if (!isSuccess(result)) throw new Error(`getOpenId 失败: ${result.message || bizCode(result)}`);
const data = result.data || {};
const id = Number(data.userInfo?.id || 0);
if (!id) {
throw new Error("该微信号还没在骁友会注册(userInfo.id=0),先在小程序里完成注册/授权手机号");
}
this.userId = id;
this.sessionKey = data.sessionKey || "";
this.openId = data.openId || "";
return true;
}
/** 只读校验会话:/api/user/info 通了就说明 userId+sessionKey 还有效 */
async loadUserInfo() {
const result = await this.request(EP_USER_INFO, { data: { userId: this.userId }, retry: false });
if (!isSuccess(result) || !result.data?.id) return false;
this.userInfo = result.data;
return true;
}
async login(force = false) {
if (this.legacySession) {
if (!this.userId || !this.sessionKey) {
$.log(`账号[${this.index}]: 旧格式变量应为 sessionKey#userId`);
return false;
}
if (await this.loadUserInfo()) return true;
$.log(`账号[${this.index}]: 手填的 sessionKey 已失效,建议改用 wx_server 的 openid 自动登录`);
return false;
}
if (!this.accountId) {
$.log(`账号[${this.index}]: 变量值为空`);
return false;
}
const cached = this.cache[this.accountId];
if (!force && cached?.userId && cached?.sessionKey) {
this.userId = Number(cached.userId);
this.sessionKey = cached.sessionKey;
this.openId = cached.openId || "";
if (await this.loadUserInfo()) {
this.log("会话缓存有效");
return true;
}
this.log("会话缓存已失效,重新登录");
}
try {
await this.loginByWxCode();
} catch (e) {
$.log(`账号[${this.index}]: 登录失败 ${e.message || e}`);
return false;
}
if (!(await this.loadUserInfo())) {
$.log(`账号[${this.index}]: 登录后读取用户信息失败`);
return false;
}
this.cache = readTokenCache();
this.cache[this.accountId] = {
userId: this.userId,
sessionKey: this.sessionKey,
openId: this.openId,
nick: this.userInfo.nick || "",
updatedAt: new Date().toISOString(),
};
writeTokenCache(this.cache);
this.log("登录成功");
return true;
}
/**
* 点击埋点 —— 签到和抽奖的【前置条件】,不是可选的统计上报。
* 服务端要求先收到一条"你点了这个按钮"的埋点事件,之后那个动作才放行:
* 不打埋点 -> signIn 恒回 40001「登录过期」、getLuck 恒回 code=1「非法请求」
* 打了埋点 -> 两个都 200
* 真机每次点按钮都会先发它,所以线上永远不会撞到;脚本不发就一直被拒。
*/
async buryPoint(page) {
const P = {
sign: { urlPath: "pages/task-center/index", urlName: "任务中心", elementName: "每日签到" },
wheel: { urlPath: "pages/wheel/index", urlName: "幸运大转盘", elementName: "立即抽奖" },
}[page];
const result = await this.request(EP_BURY_POINT, {
method: "POST",
data: {
userId: this.userId,
openId: this.openId || "",
activitySource: "Xcx_MeiRiRenWu",
urlPath: P.urlPath,
urlName: P.urlName,
elementName: P.elementName,
elementType: "页面",
eventNameEn: "MPClick",
},
retry: false,
});
// 真机点完按钮到发请求有个自然间隔,太快容易被判成非人工
await $.wait(400 + Math.floor(Math.random() * 700));
return isSuccess(result);
}
/** pages/task-center/index.js:200-235 signList -> isSignToday==0 才 signIn */
async signTask() {
const list = await this.request(EP_SIGN_LIST, { data: { userId: this.userId } });
if (!isSuccess(list)) {
this.log(`❌ 读取签到状态失败: ${list.message || bizCode(list)}`);
return;
}
const info = list.data || {};
if (Number(info.isSignToday) === 1) {
this.log(`✅ 今日已签到(本月连续 ${info.signContinuityMonth || 0} 天)`);
return;
}
if (!(await this.buryPoint("sign"))) {
this.log("⚠️ 签到前置埋点没成功,继续试签到(大概率会被拒)");
}
// retry:false —— 这里的 40001 不是会话过期,是前置埋点没到位,重登没用
const result = await this.request(EP_SIGN_IN, { data: { userId: this.userId }, retry: false });
if (isSuccess(result) && result.data) {
// state 1/2/3 在源码里对应三种「签到成功」弹窗
this.log(`✅ 签到成功 芯动值+${result.data.coreCoin ?? 0}`);
} else if (isAlreadyDone(result.message)) {
this.log(`✅ 今日已签到(${result.message})`);
} else if (bizCode(result) === CODE_SESSION_EXPIRED) {
this.log("❌ 签到回 40001:前置埋点没被服务端认下,稍后重试或在小程序里手点一次");
} else {
this.log(`❌ 签到失败: ${result.message || bizCode(result)}`);
}
}
/**
* pages/wheel —— 只抽每日免费的那次。
* 已玩次数 = luckDrawSumCount - luckDrawCount,超过 freeCountDay 后每抽要扣
* luckCoreCoin 芯动值,脚本不替用户花积分。
*/
async drawTask() {
// 转盘页的读接口也吃这个前置:不打埋点连 luckDraw/list 都回 code=1
if (!(await this.buryPoint("wheel"))) {
this.log("⚠️ 抽奖前置埋点没成功,继续试(大概率会被拒)");
}
const list = await this.request(EP_DRAW_LIST, {
data: { page: 1, userId: this.userId, activityId: LUCK_ACTIVITY_ID },
});
if (!isSuccess(list)) {
this.log(`❌ 读取抽奖信息失败: ${list.message || bizCode(list)}`);
return;
}
const d = list.data || {};
const used = Number(d.luckDrawSumCount || 0) - Number(d.luckDrawCount || 0);
const free = Number(d.freeCountDay || 0);
if (Number(d.luckDrawCount || 0) <= 0) {
this.log("🎡 抽奖次数已用完,跳过");
return;
}
if (used >= free) {
this.log(`🎡 免费次数已用完(今日已抽 ${used}/${free}),再抽要扣 ${d.luckCoreCoin} 芯动值,跳过`);
return;
}
const result = await this.request(EP_GET_LUCK, {
method: "POST",
data: { userId: this.userId, activityId: LUCK_ACTIVITY_ID },
});
if (isSuccess(result) && result.data) {
this.log(`🎉 抽奖成功: ${result.data.name || "已中奖"}`);
} else if (isAlreadyDone(result.message)) {
this.log(`🎡 今日抽奖已完成(${result.message})`);
} else if (/非法请求/.test(String(result.message || ""))) {
// 实测就是前置埋点没到位(打了埋点同一请求立刻 200 并中奖)
this.log("🎡 抽奖回「非法请求」:前置埋点没被认下,稍后重试");
} else {
this.log(`❌ 抽奖失败: ${result.message || bizCode(result)}`);
}
}
/** 取资讯列表(只读),阅读任务和点赞任务共用 */
async fetchArticles() {
const result = await this.request(EP_ARTICLES, {
data: {
page: 1,
size: 20,
userId: this.userId,
type: 0,
searchDate: "",
articleShowPlace: "骁友资讯列表页",
},
});
if (!isSuccess(result)) {
this.log(`❌ 读取资讯列表失败: ${result.message || bizCode(result)}`);
return [];
}
return result.data?.articleList || [];
}
/** 每日点赞文章 —— pages/article-details/index.js like(): articleLike(String(id), userId) */
async likeTask(articles) {
const target = articles.find((a) => Number(a.isLike) === 0);
if (!target) {
this.log("👍 列表里的文章都点过赞了,跳过");
return;
}
const result = await this.request(EP_ARTICLE_LIKE, {
data: { articleId: String(target.id), userId: this.userId },
});
if (isSuccess(result)) {
this.log(`👍 点赞成功: ${cut(target.title)}`);
} else if (isAlreadyDone(result.message)) {
this.log(`👍 已点赞(${result.message})`);
} else {
this.log(`❌ 点赞失败: ${result.message || bizCode(result)}`);
}
}
/**
* 每日阅读文章 5 分钟。
* enterReadDaily / exitReadDaily 都只收 {articleId,userId},没有时长参数——
* 时长是服务端按两次调用的时间差算的,所以必须真等 READ_SECONDS 秒。
*/
async readTask(articles) {
const target = articles.find((a) => Number(a.lookTimes || 0) < READ_SECONDS);
if (!target) {
this.log("📖 列表里的文章阅读时长都够了,跳过");
return;
}
this.log(`📖 开始阅读: ${cut(target.title)}(已读 ${target.lookTimes || 0}s)`);
const enter = await this.request(EP_ENTER_READ, {
method: "POST",
data: { articleId: target.id, userId: this.userId },
});
if (!isSuccess(enter)) {
this.log(`❌ 进入阅读失败: ${enter.message || bizCode(enter)}`);
return;
}
const wait = READ_SECONDS + 5;
this.log(`⏳ 停留 ${wait}s(源码要求满 5 分钟)`);
await $.wait(wait * 1000);
const exit = await this.request(EP_EXIT_READ, {
method: "POST",
data: { articleId: target.id, userId: this.userId },
});
if (isSuccess(exit)) {
this.log("✅ 阅读任务已提交");
} else {
this.log(`❌ 退出阅读失败: ${exit.message || bizCode(exit)}`);
}
}
/** 每日观看骁友 VLOG 1 分钟 —— pages/vlog/detail.js,进入前先看任务开关 */
async vlogTask() {
const cfg = await this.request(EP_SYS_CONFIG, { method: "POST", data: { propertyKey: VLOG_SWITCH_KEY } });
if (isSuccess(cfg) && String(cfg.data?.propertyValue) === "0") {
this.log("🎬 VLOG 任务开关关闭,跳过");
return;
}
const list = await this.request(EP_VLOG_LIST, {
data: { page: 1, size: 20, userId: this.userId, sortBy: 1 },
});
if (!isSuccess(list)) {
this.log(`❌ 读取 VLOG 列表失败: ${list.message || bizCode(list)}`);
return;
}
const records = list.data?.records || [];
const target = records.find((v) => Number(v.lookTimes || 0) < VLOG_SECONDS);
if (!target) {
this.log("🎬 列表里的 VLOG 观看时长都够了,跳过");
return;
}
this.log(`🎬 开始观看: ${cut(target.title)}(已看 ${target.lookTimes || 0}s)`);
const enter = await this.request(EP_ENTER_READ, {
method: "POST",
data: { articleId: target.id, userId: this.userId },
});
if (!isSuccess(enter)) {
this.log(`❌ 进入 VLOG 失败: ${enter.message || bizCode(enter)}`);
return;
}
await this.request(EP_VLOG_PLAY, { data: { articleId: target.id } });
const wait = VLOG_SECONDS + 5;
this.log(`⏳ 停留 ${wait}s(源码要求满 1 分钟)`);
await $.wait(wait * 1000);
const exit = await this.request(EP_EXIT_READ, {
method: "POST",
data: { articleId: target.id, userId: this.userId },
});
if (isSuccess(exit)) {
this.log("✅ VLOG 任务已提交");
} else {
this.log(`❌ 退出 VLOG 失败: ${exit.message || bizCode(exit)}`);
}
}
/**
* 互动答题(+20):只读提示,不自动作答。
* /api/interactQuestion/detail 只回题干和选项,不带正确答案;每天只有一次机会,
* 随机蒙一个会白白浪费掉,所以这里把题目打出来让人自己在小程序里答。
*/
async quizNotice() {
const result = await this.request(EP_QUIZ_DETAIL, { method: "POST", data: { userId: this.userId } });
if (!isSuccess(result)) return;
const data = result.data || {};
if (Number(data.state) > 0) {
const correct = Number(data.questionAnswerRecord?.isCorrect) === 1;
this.log(`📝 互动答题: 今日已作答(${correct ? "答对" : "答错"})`);
return;
}
const q = data.question;
if (!q) return;
const options = (q.answers || []).map((a) => `${a.answerNo}.${a.answer}`).join(" ");
this.log(`📝 互动答题未作答(+20),接口不返回正确答案,需自己在小程序里答:`);
this.log(` ${cut(q.question, 80)}`);
if (options) this.log(` ${options}`);
}
/** 收尾对账:taskDaily 的 status 1 = 已完成 */
async summary() {
const result = await this.request(EP_TASK_DAILY, { data: { userId: this.userId } });
if (!isSuccess(result)) return;
const rows = (result.data || []).map((t) => `${Number(t.status) === 1 ? "✔" : "✘"}${stripHtml(t.name)}`);
if (rows.length) this.log(`每日任务: ${rows.join(" ")}`);
const info = await this.request(EP_USER_INFO, { data: { userId: this.userId } });
if (isSuccess(info) && info.data) {
this.log(`💰 芯动值 ${info.data.coreCoin} | 等级 ${info.data.levelName || info.data.level}`);
}
}
async run() {
if (!(await this.login())) return;
this.log(`【${this.userInfo.nick || "-"}】${maskPhone(this.userInfo.phone)} 芯动值 ${this.userInfo.coreCoin}`);
await this.signTask();
await this.drawTask();
const articles = await this.fetchArticles();
if (articles.length) await this.likeTask(articles);
await this.quizNotice();
if (READ_TASK) {
if (articles.length) await this.readTask(articles);
await this.vlogTask();
} else {
this.log("⏭ wx_xlxyh_read_task=0,跳过阅读 5 分钟 / VLOG 1 分钟");
}
await this.summary();
}
}
!(async () => {
$.checkEnv(ckName);
if (!$.userCount) {
$.log(`未找到变量【${ckName}】:填 wx_server 里的 openid,多账号用 & 或换行`);
return;
}
if (!process.env.wx_auth) $.log("提示: 未配置 wx_auth,只能用旧格式 sessionKey#userId 运行");
for (const user of $.userList) {
try {
await new Task(user).run();
} catch (e) {
$.log(`账号处理异常: ${e.message || e}`);
}
}
})()
.catch((e) => $.log(`脚本异常: ${e.message || e}`))
.finally(() => $.done());