添加 js/nfsq.js
This commit is contained in:
parent
747176c1cd
commit
9633368cc7
504
js/nfsq.js
Normal file
504
js/nfsq.js
Normal file
@ -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);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user