diff --git a/js/nfsq.js b/js/nfsq.js new file mode 100644 index 0000000..f71de30 --- /dev/null +++ b/js/nfsq.js @@ -0,0 +1,504 @@ +/* + * 农夫山泉 - 全能版 (支持青龙/圈X/Surge/Loon) + * + * [功能说明] + * 1. 自动完成浏览、分享、视频号等任务 + * 2. 自动抽奖并统计 + * 3. 支持 Quantumult X 抓包自动记录账号 + * 4. 支持抓包后自动推送到青龙面板 (需配置面板信息) + * + * [环境变量 / 账号配置] + * 变量名: NFSQ_ACCOUNTS + * 格式: JSON 数组 + * [ + * { + * "uid": "xxx", + * "token": "xxx", + * "body": { ... }, + * "headers_json": { ... } + * } + * ] + * + * [Quantumult X 配置] + * + * [rewrite_local] + * # 抓取账号信息 + * ^https:\/\/sxs-consumer\.nfsq\.com\.cn\/geement\.marketinglottery\/api\/v1\/marketinglottery url script-request-body nfsq_ql.js + * + * [task_local] + * # 定时运行 (每天 9:00 运行) + * 0 9 * * * nfsq_ql.js, tag=农夫山泉, img-url=https://raw.githubusercontent.com/Orz-3/mini/master/Color/nfsq.png, enabled=true + * + * [青龙面板自动推送配置] + * 如果需要抓包后自动推送到青龙,请在下方代码区域修改 QL_CONFIG 变量 + * 或者在本地脚本管理器的“偏好设置”中存储相对应的数据 + * + */ + +// ================= 用户配置区域 ================= + +// 青龙面板对接配置 (如果不需要自动推送,可忽略) +// 建议在 BoxJs 或 脚本编辑器中修改,避免硬编码泄露 +const QL_CONFIG = { + URL: "http://chickliu.store:5735", // 例如 http://192.168.1.5:5700 + CLIENT_ID: "0g9wa9_Ulcud", // 应用管理 -> 新建应用 -> Client ID + CLIENT_SECRET: "FWNsG34ttRRTKbCxsa9_2hxh" // 应用管理 -> 新建应用 -> Client Secret +}; + +const SCRIPT_NAME = "农夫山泉"; +const ENV_NAME = "NFSQ_ACCOUNTS"; + +// ================= 运行环境兼容 ================= + +// 初始化环境 +const $ = new Env(SCRIPT_NAME); + +// 兼容 Node.js 的 fetch +let nodeHeaders = {}; +if ($.isNode()) { + try { + const https = require("https"); + if (typeof fetch !== "function") { + global.fetch = function (url, options = {}) { + return new Promise((resolve, reject) => { + const u = new URL(url); + const req = https.request( + { + hostname: u.hostname, + path: u.pathname + u.search, + method: options.method || "GET", + headers: options.headers || {}, + }, + (res) => { + let rawData = ""; + res.on("data", (chunk) => (rawData += chunk)); + res.on("end", () => { + resolve({ + status: res.statusCode, + ok: res.statusCode >= 200 && res.statusCode < 300, + headers: res.headers, + json: () => Promise.resolve(rawData ? JSON.parse(rawData) : {}), + text: () => Promise.resolve(rawData) + }); + }); + } + ); + req.on("error", reject); + if (options.body) req.write(options.body); + req.end(); + }); + }; + } + } catch (e) { } +} + +const TASK_LIST = [ + { name: "浏览得机会", id: "2510301515552" }, + { name: "分享得机会", id: "2510301516431" }, + { name: "观看视频号", id: "2510301517291" }, + { name: "浏览公众号", id: "2510301518121" } +]; + +const RECORD_URL = "https://sxs-consumer.nfsq.com.cn/geement.actjextra/api/v1/act/win/goods/simple?act_codes=ACT2510301507191%2CACT2510301505581"; + +// ================= 入口逻辑 ================= + +(async () => { + // 1. 抓包模式 + if (typeof $request !== "undefined") { + await handleRewrite(); + } + // 2. 运行模式 + else { + await main(); + } + $.done(); +})(); + +// ================= 核心逻辑 ================= + +// 抓包处理 +async function handleRewrite() { + try { + $.log("🔔 检测到抽奖请求,开始抓取账号信息..."); + + // 1. 获取请求头和体 + const headers = $request.headers || {}; + const bodyStr = $request.body || "{}"; + + // 兼容不同客户端 Header key 大小写 + const getHeader = (key) => headers[key] || headers[key.toLowerCase()] || headers[Object.keys(headers).find(k => k.toLowerCase() === key.toLowerCase())]; + + const uid = getHeader('unique_identity'); + const token = getHeader('apitoken') || getHeader('apiToken'); + + if (!uid || !token) { + $.msg(SCRIPT_NAME, "抓包失败", "未找到 unique_identity 或 apitoken"); + return; + } + + // 2. 组装账号 + let bodyObj = {}; + try { bodyObj = JSON.parse(bodyStr); } catch (e) { } + + // 构建标准账号对象 + const newAccount = { + uid: uid, + token: token, + body: bodyObj, + headers_json: headers, + timestamp: new Date().getTime(), + remark: `自动抓包 ${new Date().toLocaleString()}` + }; + + // 3. 读取现有账号列表 + let accounts = []; + const cachedData = $.getdata(ENV_NAME); + if (cachedData) { + try { + // 兼容可能被存为 JSON 字符串的情况 + const parsed = JSON.parse(cachedData); + accounts = Array.isArray(parsed) ? parsed : [parsed]; + } catch (e) { + // 如果解析失败,说明格式不对,覆盖即可 + accounts = []; + } + } + + // 4. 更新或添加账号 + const idx = accounts.findIndex(u => u.uid === uid); + if (idx > -1) { + accounts[idx] = newAccount; // 更新 + $.log(`♻️ 更新旧账号: ${uid}`); + } else { + accounts.push(newAccount); // 新增 + $.log(`🆕 添加新账号: ${uid}`); + } + + // 5. 保存到本地 + const saved = $.setdata(JSON.stringify(accounts, null, 2), ENV_NAME); + if (saved) { + $.msg(SCRIPT_NAME, `✅ 账号抓取成功`, `当前共 ${accounts.length} 个账号`); + } else { + $.msg(SCRIPT_NAME, `❌ 保存失败`, `BoxJs/本地存储 写入失败`); + } + + // 6. 推送到青龙 (如果有配置) + if (QL_CONFIG.URL && QL_CONFIG.CLIENT_ID && QL_CONFIG.CLIENT_SECRET) { + await pushToQingLong(accounts); + } else { + $.log("⚠️ 未配置青龙信息,跳过推送。(请在脚本 QL_CONFIG 中填写,或手动复制 BoxJs 数据)"); + } + + } catch (e) { + console.log("❌ 抓包处理异常:", e); + $.msg(SCRIPT_NAME, "抓包异常", e.message); + } +} + +// 任务运行主函数 +async function main() { + // 优先读取本地 BoxJs/Prefs 数据,其次读取环境变量 + let accountsStr = $.getdata(ENV_NAME); + if (!accountsStr && $.isNode()) { + accountsStr = process.env[ENV_NAME]; + } + + let list = []; + if (!accountsStr) { + console.log(`❌ 未找到账号信息!`); + console.log(`1. 青龙用户:请设置环境变量 ${ENV_NAME}`); + console.log(`2. 圈X用户:请先运行一次抓包 (重写已配置)`); + return; + } + + try { + list = JSON.parse(accountsStr); + if (!Array.isArray(list)) list = [list]; + } catch (e) { + // 尝试修复单引号 + try { + list = JSON.parse(accountsStr.replace(/'/g, '"')); + } catch (e2) { + console.log("❌ JSON 解析失败,请检查数据格式"); + return; + } + } + + console.log(`🚀 脚本启动: 检测到 ${list.length} 个账号`); + + for (let i = 0; i < list.length; i++) { + let acc = list[i]; + console.log(`\n======== 👤 账号 ${i + 1}/${list.length} (${acc.uid?.slice(0, 6) || '未知'}) ========`); + + if (!acc.token || !acc.uid) { + console.log(" ⚠️ 账号信息缺失(token/uid),跳过"); + continue; + } + + let realUA = extractUA(acc.headers_json); + + // --- 1. 做任务 --- + console.log("【1️⃣ 任务环节】"); + for (let task of TASK_LIST) { + await joinTask(acc.token, acc.uid, task, realUA); + await $.wait(500); + } + + // --- 2. 抽奖 --- + console.log("\n【2️⃣ 抽奖环节】"); + let count = 0; + let running = true; + while (running && count < 50) { // 限制最大50次防止死循环 + count++; + let status = await doLottery(acc.token, acc.uid, acc.body, acc.headers_json); + if (status !== "CONTINUE") { + running = false; + } else { + process.stdout.write("."); // 进度条效果 + await $.wait(2000); // 间隔2秒 + } + } + console.log(count >= 50 ? "\n ⚠️ 达到最大抽奖次数限制" : ""); + + // --- 3. 查奖 --- + console.log("\n【3️⃣ 历史奖品】"); + await checkAllPrizes(acc.token, acc.uid, realUA); + + await $.wait(1500); + } + console.log("\n🏁 所有任务执行结束"); +} + +// ================= 业务函数 ================= + +async function doLottery(token, uid, body, headers_obj) { + // 准备请求体 + let payload = typeof body === 'string' ? body : JSON.stringify(body || {}); + // 准备请求头 + let headers = { + 'apitoken': token, + 'unique_identity': uid, + 'Host': 'sxs-consumer.nfsq.com.cn', + 'Content-Type': 'application/json', + 'User-Agent': extractUA(headers_obj) || "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.18" + }; + + try { + const resp = await $.http.post({ + url: 'https://sxs-consumer.nfsq.com.cn/geement.marketinglottery/api/v1/marketinglottery', + headers: headers, + body: payload + }); + + const res = JSON.parse(resp.body || "{}"); + if (res.code === 200) { + let pName = res.data?.prizedto?.prize_name || res.data?.prize_name || "未知奖品"; + console.log(` 🎉 中奖: ${pName}`); + return "CONTINUE"; + } else { + // 失败或无资格 + const msg = res.msg || "未知错误"; + // 只有特定错误才停止,其他(如网络抖动)可以重试,但这里简单起见,非200一般都停 + if (msg.includes("不足") || msg.includes("上限") || msg.includes("机会")) { + console.log(` ⏹ 停止: ${msg}`); + return "STOP"; + } + console.log(` 💨 未中/继续: ${msg}`); + return "CONTINUE"; + } + } catch (e) { + console.log(` ❌ 抽奖出错: ${e.message}`); + return "STOP"; + } +} + +async function joinTask(token, uid, task, userAgent) { + const timeStr = encodeURIComponent(getNowFormatDate()); + const url = `https://sxs-consumer.nfsq.com.cn/geement.marketingplay/api/v1/task/join?action_time=${timeStr}&task_id=${task.id}`; + const headers = { + 'apitoken': token, + 'unique_identity': uid, + 'Host': 'sxs-consumer.nfsq.com.cn', + 'Referer': 'https://servicewechat.com/wxd79ec05386a78727/100/page-frame.html', + 'User-Agent': userAgent || "Mozilla/5.0" + }; + + try { + const resp = await $.http.get({ url, headers }); + const res = JSON.parse(resp.body || "{}"); + if (res.code === 200 || (res.msg && res.msg.includes("已参与"))) { + console.log(` ✅ ${task.name}: 完成`); + } else { + console.log(` ⏺ ${task.name}: ${res.msg}`); + } + } catch (e) { + console.log(` ❌ ${task.name}: 请求失败`); + } +} + +async function checkAllPrizes(token, uid, userAgent) { + const headers = { + 'apitoken': token, + 'unique_identity': uid, + 'Host': 'sxs-consumer.nfsq.com.cn', + 'User-Agent': userAgent || "Mozilla/5.0" + }; + + try { + const resp = await $.http.get({ url: RECORD_URL, headers }); + const res = JSON.parse(resp.body || "{}"); + if (res.code === 200 && res.data && res.data.length > 0) { + console.log(` 📚 共 ${res.data.length} 条记录:`); + res.data.forEach(item => { + console.log(` 🎁 [${item.scan_time}] ${item.win_prize_name}`); + }); + } else { + console.log(" 💨 暂无中奖记录"); + } + } catch (e) { + console.log(" ❌ 查询失败"); + } +} + +// ================= 青龙推送相关 ================= + +async function pushToQingLong(accounts) { + $.log("📤 正在推送到青龙面板..."); + const { URL, CLIENT_ID, CLIENT_SECRET } = QL_CONFIG; + + // 1. 获取 Token + const tokenUrl = `${URL}/open/auth/token?client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}`; + try { + const tReq = await $.http.get({ url: tokenUrl }); + const tBody = JSON.parse(tReq.body); + if (tBody.code !== 200) { + $.msg(SCRIPT_NAME, "青龙推送失败", "获取 Token 失败,请检查 Client ID/Secret"); + return; + } + const qlToken = tBody.data.token; + + // 2. 获取现有环境变量 + const envsUrl = `${URL}/open/envs?searchValue=${ENV_NAME}`; + const eReq = await $.http.get({ + url: envsUrl, + headers: { 'Authorization': `Bearer ${qlToken}` } + }); + const eBody = JSON.parse(eReq.body); + const exists = eBody.data && eBody.data.length > 0; + + const newValue = JSON.stringify(accounts); + + if (exists) { + // 更新 + const envId = eBody.data[0].id; + const updateUrl = `${URL}/open/envs`; + const uReq = await $.http.put({ + url: updateUrl, + headers: { + 'Authorization': `Bearer ${qlToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + id: envId, + name: ENV_NAME, + value: newValue, + remarks: "自动抓包更新: " + new Date().toLocaleString() + }) + }); + $.log(" ✅ 已更新青龙环境变量"); + $.msg(SCRIPT_NAME, "推送成功", "已更新青龙环境变量 " + ENV_NAME); + } else { + // 新建 + const addUrl = `${URL}/open/envs`; + const aReq = await $.http.post({ + url: addUrl, + headers: { + 'Authorization': `Bearer ${qlToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify([{ + name: ENV_NAME, + value: newValue, + remarks: "自动抓包创建" + }]) + }); + $.log(" ✅ 已新建青龙环境变量"); + $.msg(SCRIPT_NAME, "推送成功", "已新建青龙环境变量 " + ENV_NAME); + } + + } catch (e) { + $.log(" ❌ 推送异常: " + e.message); + $.msg(SCRIPT_NAME, "青龙推送异常", e.message); + } +} + +// ================= 工具函数 & Env实现 ================= + +function extractUA(headers) { + if (!headers) return null; + if (typeof headers !== 'object') { + try { headers = JSON.parse(headers); } catch (e) { return null; } + } + return headers['User-Agent'] || headers['user-agent']; +} +function getNowFormatDate() { + let d = new Date(); + let pad = (n) => n < 10 ? '0' + n : n; + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; +} + +// 兼容性封装 (Copy from common env implementations) +function Env(name) { + return new class { + constructor(name) { this.name = name; this.logs = []; } + isNode() { return "undefined" != typeof module && !!module.exports } + log(...t) { if (t.length > 0) { this.logs = [...this.logs, ...t]; console.log(t.join(" ")); } } + msg(t, e, s) { + if (this.isNode()) { this.log(t, e, s); } + else { if ("undefined" != typeof $notify) $notify(t, e, s); } + } + getdata(t) { + if (this.isNode()) return process.env[t]; + if ("undefined" != typeof $prefs) return $prefs.valueForKey(t); + if ("undefined" != typeof $persistentStore) return $persistentStore.read(t); + return null; + } + setdata(t, e) { + if (this.isNode()) return false; + if ("undefined" != typeof $prefs) return $prefs.setValueForKey(t, e); + if ("undefined" != typeof $persistentStore) return $persistentStore.write(t, e); + return false; + } + wait(t) { return new Promise(e => setTimeout(e, t)) } + done() { if (this.isNode()) { } else { $done({}) } } + + // 简单封装 HTTP,消除平台差异 + get http() { + const that = this; + return { + get: (opts) => that.sendReq('GET', opts), + post: (opts) => that.sendReq('POST', opts) + } + } + sendReq(method, opts) { + return new Promise((resolve, reject) => { + const reqOpts = { url: opts.url, method: method, headers: opts.headers, body: opts.body }; + if (this.isNode()) { + // Node fetch 在外部已定义,这里直接用 + fetch(reqOpts.url, reqOpts).then(res => res.text().then(txt => resolve({ body: txt, headers: res.headers }))).catch(reject); + } else if ("undefined" != typeof $task) { + $task.fetch(reqOpts).then(res => resolve({ body: res.body, headers: res.headers })).catch(reject); + } else if ("undefined" != typeof $httpClient) { + const client = method === 'POST' ? $httpClient.post : $httpClient.get; + client(reqOpts, (err, res, body) => { + if (err) reject(err); + else resolve({ body: body, headers: res.headers }); + }); + } else { + reject("Env not supported"); + } + }); + } + }(name); +}