|
| 1 | +/* |
| 2 | +------------------------------------------ |
| 3 | +@Description: 拼多多果园公众号版(多多果园) - 微信小程序 code 静默登录 + 每日签到(+浇水) |
| 4 | +cron: 28 9,13 * * * |
| 5 | +------------------------------------------ |
| 6 | +变量名:pddgy |
| 7 | +变量值:wx_server 里的 openid/账号标识,多账号用 & 或换行分隔(可加 #备注) |
| 8 | +
|
| 9 | +依赖变量: |
| 10 | +wx_server_url 默认 http://192.168.31.196:8787 |
| 11 | +wx_auth 必填,wx_server 鉴权值 |
| 12 | +------------------------------------------ |
| 13 | +契约(appid wx839691cce7c102bb,重风控站点): |
| 14 | +
|
| 15 | +
|
| 16 | +登录 GET https://mobile.yangkeduo.com/garden_index_lz_0.html?_pdd_fs=1&_pdd_tc=676666&_pdd_sbs=1&fun_id=wechat_app_home&__wls_rt=1&__wls_lt=1&__wls_fm=b&code=${code}&state=BASE |
| 17 | + Cookie = PDDAccessToken=<access_token>; pdd_user_id=<uid>; pdd_user_uin=<uin>; <Set-Cookie> |
| 18 | + 鉴权=纯 Cookie(业务请求无 HMAC/签名),业务 URL 追加 ?pdduid=<uid> |
| 19 | + 风控:暂无 |
| 20 | +首页 POST /proxy/api/api/manor-query/proxy/home/page {..., tubetoken} |
| 21 | + -> tubetoken(签到/浇水必需) / water_amount;error_code 40001 = Cookie 失效 |
| 22 | +签到 POST /proxy/api/api/manor/common/apply/activity?pdduid=<uid> |
| 23 | + body = {type:201811, params:{ui_id:3,type:2}, fun_id:"wechat_app_home", tubetoken, fun_pl:2} |
| 24 | + -> error_code==0 / success==true 成功;已签常见 error_msg 含「已签/重复/已领取」 |
| 25 | +浇水 POST /proxy/api/api/manor/water/cost {..., cost_water_amount:10, tubetoken}(附带,非核心,失败不影响签到判定) |
| 26 | +app_id=33 / xcx_version / activity type 201811 均为该小程序固定应用常量(原脚本硬编码,非个人凭证)。 |
| 27 | +业务基址 https://mobile.yangkeduo.com/proxy/api/api ,登录基址 https://api.pinduoduo.com 。 |
| 28 | +------------------------------------------ |
| 29 | +*/ |
| 30 | + |
| 31 | +const { Env } = require("../tools/env.js"); |
| 32 | +const $ = new Env("拼多多果园公众号版"); |
| 33 | +const axios = require("axios"); |
| 34 | +const fs = require("fs"); |
| 35 | +const path = require("path"); |
| 36 | +const WeChatServer = require("./wcs.js"); |
| 37 | + |
| 38 | +const ckName = "pddgy_gzh"; |
| 39 | +const MINI_APP_ID = "wx839691cce7c102bb"; |
| 40 | +const XCX_VERSION = "v8.6.21"; |
| 41 | +const PDD_APP_ID = 33; |
| 42 | +const API_BASE = "https://api.pinduoduo.com"; |
| 43 | +const ORCHARD_BASE = "https://mobile.yangkeduo.com"; |
| 44 | +const MANOR_BASE = ORCHARD_BASE + "/proxy/api/api"; |
| 45 | +const CHECKIN_TYPE = 201811; |
| 46 | +const MAX_WATER_TIMES = 20; // 附带浇水上限(非核心) |
| 47 | +const TOKEN_CACHE_FILE = path.join(__dirname, "pddgy_token_cache.json"); |
| 48 | +const UA = |
| 49 | + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 " + |
| 50 | + "MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF XWEB/19895 miniProgram/wx32540bd863b27570"; |
| 51 | + |
| 52 | +const wechat = new WeChatServer({ |
| 53 | + url: process.env.wx_server_url || "http://192.168.31.196:8787", |
| 54 | + appid: MINI_APP_ID, |
| 55 | + auth: process.env.wx_auth || "", |
| 56 | +}); |
| 57 | + |
| 58 | +function readCache() { |
| 59 | + try { if (!fs.existsSync(TOKEN_CACHE_FILE)) return {}; return JSON.parse(fs.readFileSync(TOKEN_CACHE_FILE, "utf8")) || {}; } catch (e) { return {}; } |
| 60 | +} |
| 61 | +function writeCache(c) { |
| 62 | + try { fs.writeFileSync(TOKEN_CACHE_FILE, JSON.stringify(c, null, 2), "utf8"); } catch (e) { $.log(`写入缓存失败: ${e.message || e}`); } |
| 63 | +} |
| 64 | +function parseAccount(raw = "") { |
| 65 | + const [id, remark] = String(raw).split("#").map((s) => (s || "").trim()); |
| 66 | + return { openid: id, remark: remark || "" }; |
| 67 | +} |
| 68 | +function short(v, n = 240) { |
| 69 | + const t = typeof v === "string" ? v : JSON.stringify(v); |
| 70 | + return !t ? "" : t.length > n ? `${t.slice(0, n)}...` : t; |
| 71 | +} |
| 72 | +function asObj(d) { |
| 73 | + if (d && typeof d === "object") return d; |
| 74 | + if (typeof d === "string") { try { return JSON.parse(d); } catch { return {}; } } |
| 75 | + return {}; |
| 76 | +} |
| 77 | +function extractUid(cookieStr) { |
| 78 | + const m = /pdd_user_id=(\d+)/.exec(cookieStr || ""); |
| 79 | + return m ? m[1] : ""; |
| 80 | +} |
| 81 | + |
| 82 | +function okCode(res) { |
| 83 | + if (!res) return false; |
| 84 | + if (res.success === true) return true; |
| 85 | + if (Number(res.error_code) === 0 && (res.error_code === 0 || res.error_code === "0")) return true; |
| 86 | + if (res.code === 0) return true; |
| 87 | + return false; |
| 88 | +} |
| 89 | +function respMsg(res) { |
| 90 | + if (!res) return ""; |
| 91 | + return res.error_msg || res.msg || res.message || res.errorMsg || ""; |
| 92 | +} |
| 93 | + |
| 94 | +function get_accessToken(cookieStr) { |
| 95 | + const m = /PDDAccessToken=(\d+)/.exec(cookieStr || ""); |
| 96 | + return m ? m[1] : ""; |
| 97 | +} |
| 98 | +function get_pdd_user_id(cookieStr) { |
| 99 | + const m = /pdd_user_id=(\d+)/.exec(cookieStr || ""); |
| 100 | + return m ? m[1] : ""; |
| 101 | +} |
| 102 | +function get_pdd_user_uin(cookieStr) { |
| 103 | + const m = /pdd_user_uin=(\d+)/.exec(cookieStr || ""); |
| 104 | + return m ? m[1] : ""; |
| 105 | +} |
| 106 | + |
| 107 | +class Task { |
| 108 | + constructor(raw) { |
| 109 | + this.index = $.userIdx++; |
| 110 | + this.account = parseAccount(raw); |
| 111 | + this.cookieStr = ""; |
| 112 | + this.pdduid = ""; |
| 113 | + this.tubetoken = ""; |
| 114 | + this.water = 0; |
| 115 | + } |
| 116 | + log(text) { |
| 117 | + $.log(`账号[${this.index}]${this.account.remark ? `[${this.account.remark}]` : ""} ${text}`); |
| 118 | + } |
| 119 | + async getCode() { |
| 120 | + const { data } = await wechat.getCode(this.account.openid); |
| 121 | + if (data && data.status === false) throw new Error(`wx_server 取code失败: ${data.message || short(data)}`); |
| 122 | + const code = data?.data?.code || data?.code; |
| 123 | + if (!code || typeof code !== "string") throw new Error(`wx_server 未返回 code: ${short(data)}`); |
| 124 | + return code; |
| 125 | + } |
| 126 | + async login() { |
| 127 | + const code = await this.getCode(); |
| 128 | + this.log(`获取code: ${short(code, 12)}...`); |
| 129 | + const loginBody = { |
| 130 | + code, |
| 131 | + has_auth: false, |
| 132 | + app_id: PDD_APP_ID, |
| 133 | + support_enhance_type: 3, |
| 134 | + xcx_version: XCX_VERSION, |
| 135 | + }; |
| 136 | + const res = await axios.request({ |
| 137 | + method: "GET", |
| 138 | + url: `https://mobile.yangkeduo.com/garden_index_lz_0.html?_pdd_fs=1&_pdd_tc=676666&_pdd_sbs=1&fun_id=wechat_app_home&__wls_rt=1&__wls_lt=1&__wls_fm=b&code=${code}&state=BASE`, |
| 139 | + //data: JSON.stringify(loginBody), |
| 140 | + headers: { |
| 141 | + "Content-Type": "application/json;charset=UTF-8", |
| 142 | + "User-Agent": UA, |
| 143 | + Accept: "*/*", |
| 144 | + Referer: `https://servicewechat.com/${MINI_APP_ID}/1840/page-frame.html`, |
| 145 | + "x-xcx-queries": `mini_program_name=pdd;mp_theme_version=${XCX_VERSION}`, |
| 146 | + xweb_xhr: "1", |
| 147 | + }, |
| 148 | + timeout: 20000, |
| 149 | + validateStatus: () => true, |
| 150 | + }); |
| 151 | + |
| 152 | + let setCookieStr = ""; |
| 153 | + const sc = res.headers && res.headers["set-cookie"]; |
| 154 | + if (sc) { |
| 155 | + const arr = Array.isArray(sc) ? sc : [sc]; |
| 156 | + setCookieStr = arr.map((c) => String(c).split(";")[0]).join("; "); |
| 157 | + } |
| 158 | + console.log(setCookieStr) |
| 159 | + // 使用示例 |
| 160 | + const accessToken = get_accessToken(setCookieStr); |
| 161 | + const uid = get_pdd_user_id(setCookieStr); |
| 162 | + const uin = get_pdd_user_uin(setCookieStr); |
| 163 | + const acid = ""; |
| 164 | + |
| 165 | + const parts = [`PDDAccessToken=${accessToken}`, `pdd_user_id=${uid}`]; |
| 166 | + if (uin) parts.push(`pdd_user_uin=${uin}`); |
| 167 | + if (acid) parts.push(`acid=${acid}`); |
| 168 | + if (setCookieStr) parts.push(setCookieStr); |
| 169 | + |
| 170 | + this.cookieStr = parts.join("; "); |
| 171 | + this.pdduid = uid; |
| 172 | + const cache = readCache(); |
| 173 | + cache[this.account.openid] = { cookieStr: this.cookieStr, uid, updatedAt: new Date().toISOString() }; |
| 174 | + writeCache(cache); |
| 175 | + this.log(`登录成功 uid=${uid}`); |
| 176 | + } |
| 177 | + async manorPost(apiPath, body) { |
| 178 | + const url = `${MANOR_BASE}${apiPath}${apiPath.includes("?") ? "&" : "?"}pdduid=${this.pdduid}`; |
| 179 | + const res = await axios.request({ |
| 180 | + method: "POST", |
| 181 | + url, |
| 182 | + data: JSON.stringify(body || {}), |
| 183 | + headers: { |
| 184 | + "User-Agent": UA, |
| 185 | + Accept: "application/json, text/plain, */*", |
| 186 | + "Content-Type": "application/json;charset=UTF-8", |
| 187 | + Origin: ORCHARD_BASE, |
| 188 | + Referer: `${ORCHARD_BASE}/garden_index_lz_0.html`, |
| 189 | + Cookie: this.cookieStr, |
| 190 | + }, |
| 191 | + timeout: 20000, |
| 192 | + validateStatus: () => true, |
| 193 | + }); |
| 194 | + return asObj(res.data); |
| 195 | + } |
| 196 | + // 首页:刷新 tubetoken + 水滴,同时用作 Cookie 有效性探测 |
| 197 | + async getHomePage() { |
| 198 | + const body = { |
| 199 | + mission_type: 0, fun_id: "wechat_app_home", message_source: null, |
| 200 | + page_type: "HOME_PAGE", push_source_mission_type: 0, fruit_config_version: "", |
| 201 | + unlock_scene_version: "", app_home_click_icon_type: null, tubetoken: this.tubetoken, |
| 202 | + push_act_source: null, need_show_home_popup: true, fun_pl: 2, |
| 203 | + }; |
| 204 | + const result = await this.manorPost("/manor-query/proxy/home/page", body); |
| 205 | + if (!result || !result.user_manor_info) { this.log(` 首页校验失败(40001),Cookie 可能已过期`); return false; } |
| 206 | + if (result.user_manor_info.tubetoken) this.tubetoken = result.user_manor_info.tubetoken; |
| 207 | + if (result.user_manor_info.water_amount != null) this.water = result.user_manor_info.water_amount; |
| 208 | + return !!this.tubetoken; |
| 209 | + } |
| 210 | + async getWater() { |
| 211 | + const result = await this.manorPost("/manor-gateway/manor/query/user/water?is_back=1", {}); |
| 212 | + return (result && result.water_amount) || 0; |
| 213 | + } |
| 214 | + async checkin() { |
| 215 | + const body = { |
| 216 | + type: CHECKIN_TYPE, |
| 217 | + params: { ui_id: 3, type: 2 }, |
| 218 | + fun_id: "wechat_app_home", |
| 219 | + tubetoken: this.tubetoken, |
| 220 | + fun_pl: 2, |
| 221 | + }; |
| 222 | + const res = await this.manorPost("/manor/common/apply/activity", body); |
| 223 | + if (okCode(res)) { |
| 224 | + const reward = res.water || res.reward_amount || (res.data && (res.data.water || res.data.reward_amount)) || ""; |
| 225 | + this.log(`✅ 签到成功${reward ? `,+${reward}水滴` : ""}`); |
| 226 | + return { ok: true }; |
| 227 | + } |
| 228 | + const msg = respMsg(res) || short(res); |
| 229 | + if (/已签|签到过|重复|已领取|已完成|already|repeat/i.test(String(msg))) { |
| 230 | + this.log(`✅ 今日已签到(${msg})`); |
| 231 | + return { ok: true, already: true }; |
| 232 | + } |
| 233 | + // 风控类反馈 |
| 234 | + if (/验证|风控|拦截|滑块|verify|forbidden/i.test(String(msg)) || Number(res.error_code) === 54002) { |
| 235 | + this.blocked = true; |
| 236 | + this.log(`⛔ 签到被风控拦截: ${msg}`); |
| 237 | + return { ok: false, blocked: true, msg }; |
| 238 | + } |
| 239 | + this.log(`❌ 签到失败: ${msg}`); |
| 240 | + return { ok: false, msg }; |
| 241 | + } |
| 242 | + // 附带浇水(非核心:失败/风控都不影响签到判定) |
| 243 | + async waterTree() { |
| 244 | + try { |
| 245 | + let water = await this.getWater(); |
| 246 | + if (water < 10) { this.log(` [浇水] 水滴 ${water} 不足10,跳过`); return; } |
| 247 | + const count = Math.min(MAX_WATER_TIMES, Math.floor(water / 10)); |
| 248 | + let watered = 0; |
| 249 | + let curr = water; |
| 250 | + for (let i = 0; i < count; i++) { |
| 251 | + const body = { |
| 252 | + atw: true, location_auth: false, last_stay_time: Math.floor(Math.random() * 40) + 10, |
| 253 | + can_trigger_random_mission: false, product_scene: 0, minor: false, |
| 254 | + ext_params: { can_trigger201824: true }, mission_type: 0, cost_water_amount: 10, |
| 255 | + merge_cost: false, fun_id: "wechat_app_home", lower_end_device: false, |
| 256 | + cost_water_competition_in_scene_icon: false, is_small_screen: true, |
| 257 | + tubetoken: this.tubetoken, fun_pl: 2, |
| 258 | + }; |
| 259 | + const r = await this.manorPost("/manor/water/cost", body); |
| 260 | + const left = r && r.now_water_amount; |
| 261 | + if (left != null && left < curr) { curr = left; watered++; if (left < 10) break; await $.wait(200, 400); } |
| 262 | + else break; |
| 263 | + } |
| 264 | + this.log(` [浇水] 完成 ${watered} 次,剩余水滴 ${curr}`); |
| 265 | + } catch (e) { |
| 266 | + this.log(` [浇水] 跳过: ${e.message || e}`); |
| 267 | + } |
| 268 | + } |
| 269 | + async ensureSession() { |
| 270 | + const cached = readCache()[this.account.openid] || {}; |
| 271 | + if (!this.cookieStr && cached.cookieStr) { |
| 272 | + this.cookieStr = cached.cookieStr; |
| 273 | + this.pdduid = cached.uid || extractUid(this.cookieStr); |
| 274 | + this.log("使用缓存Cookie"); |
| 275 | + if (await this.getHomePage()) { this.log(`缓存有效,水滴=${this.water}`); return; } |
| 276 | + this.log("缓存失效,重新登录"); |
| 277 | + this.cookieStr = ""; this.pdduid = ""; this.tubetoken = ""; |
| 278 | + } |
| 279 | + if (!this.cookieStr) { |
| 280 | + await this.login(); |
| 281 | + if (!this.pdduid) this.pdduid = extractUid(this.cookieStr); |
| 282 | + if (!(await this.getHomePage())) throw new Error("登录后首页加载失败(可能风控/Cookie无效)"); |
| 283 | + this.log(`当前水滴=${this.water}`); |
| 284 | + } |
| 285 | + } |
| 286 | + async run() { |
| 287 | + if (!this.account.openid) { this.log("跳过:变量值里没有 openid"); return; } |
| 288 | + try { |
| 289 | + await this.ensureSession(); |
| 290 | + const r = await this.checkin(); |
| 291 | + await $.wait(500, 1000); |
| 292 | + if (!r.blocked) await this.waterTree(); |
| 293 | + } catch (e) { |
| 294 | + const em = String(e.message || e); |
| 295 | + if (em.startsWith("BLOCKED")) { |
| 296 | + this.log(`⛔ ${em.replace(/^BLOCKED:/, "")}(拼多多重风控,需在小程序内手动过验证,脚本无法绕过)`); |
| 297 | + return; |
| 298 | + } |
| 299 | + this.log(`执行失败: ${em}`); |
| 300 | + } |
| 301 | + } |
| 302 | +} |
| 303 | + |
| 304 | +!(async () => { |
| 305 | + $.checkEnv(ckName); |
| 306 | + if (!$.userCount) { $.log(`未找到变量 ${ckName}`); return; } |
| 307 | + for (let i = 0; i < $.userList.length; i++) { |
| 308 | + await new Task($.userList[i]).run(); |
| 309 | + if (i < $.userList.length - 1) await $.wait(1500, 3000); |
| 310 | + } |
| 311 | +})().catch((e) => $.log(e.message || e)).finally(() => $.done()); |
0 commit comments