QX/js/nfsq.js
2025-12-08 18:21:20 +08:00

527 lines
19 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

/*
* 农夫山泉 - 全能版 (支持青龙/圈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
};
// 3. 本地存储逻辑 (BoxJs 仍然保持本地全量备份)
let localAccounts = [];
const cachedData = $.getdata(ENV_NAME);
if (cachedData) {
try {
const parsed = JSON.parse(cachedData);
localAccounts = Array.isArray(parsed) ? parsed : [parsed];
} catch (e) { localAccounts = []; }
}
const idx = localAccounts.findIndex(u => u.uid === uid);
if (idx > -1) {
localAccounts[idx] = newAccount; // 更新
} else {
localAccounts.push(newAccount); // 新增
}
const saved = $.setdata(JSON.stringify(localAccounts, null, 2), ENV_NAME);
if (saved) {
$.msg(SCRIPT_NAME, `✅ 账号抓取成功`, `UID: ${uid.slice(0, 6)}... 已保存到本地`);
}
// 4. 推送到青龙 (智能合并模式)
if (QL_CONFIG.URL && QL_CONFIG.CLIENT_ID && QL_CONFIG.CLIENT_SECRET) {
await pushToQingLong(newAccount);
} 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(accountToPush) {
$.log(`📤 正在尝试处理青龙推送 (UID: ${accountToPush.uid.slice(0, 6)}...)`);
const { URL, CLIENT_ID, CLIENT_SECRET } = QL_CONFIG;
try {
// 1. 获取 Token
const tokenUrl = `${URL}/open/auth/token?client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}`;
const tReq = await $.http.get({ url: tokenUrl });
const tBody = JSON.parse(tReq.body);
if (tBody.code !== 200) {
$.msg(SCRIPT_NAME, "青龙推送失败", "获取 Token 失败,请检查密钥");
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);
// 3. 准备数据:读取现有 -> 合并
let remoteList = [];
let existingEnv = null;
if (eBody.data && eBody.data.length > 0) {
existingEnv = eBody.data[0];
try {
// 尝试解析现有的值为 JSON 数组
// 注意:青龙里可能存的是字符串,也可能已经是 JSON
let rawValue = existingEnv.value;
// 有时候青龙返回的 value 是被转义过的字符串,尝试解析
let parsedValue = JSON.parse(rawValue);
if (Array.isArray(parsedValue)) {
remoteList = parsedValue;
} else {
// 如果不是数组,可能是单个对象,或者其他格式,清空
remoteList = [];
}
} catch (e) {
console.log(" ⚠️ 解析青龙现有变量失败,将覆盖为新列表");
remoteList = [];
}
}
// 4. 执行合并逻辑
const targetIdx = remoteList.findIndex(u => u.uid === accountToPush.uid);
let action = "";
if (targetIdx > -1) {
remoteList[targetIdx] = accountToPush;
action = "更新";
} else {
remoteList.push(accountToPush);
action = "新增";
}
const newValue = JSON.stringify(remoteList); // 保持紧凑格式,或者用 JSON.stringify(remoteList, null, 2) 美化
// 5. 推送回青龙
if (existingEnv) {
// update
const updateUrl = `${URL}/open/envs`;
await $.http.put({
url: updateUrl,
headers: {
'Authorization': `Bearer ${qlToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: existingEnv.id,
name: ENV_NAME,
value: newValue,
remarks: existingEnv.remarks || (`自动抓包更新: ${new Date().toLocaleString()}`)
})
});
$.msg(SCRIPT_NAME, `✅ 推送成功 [${action}]`, `UID: ${accountToPush.uid.slice(0, 6)}... 已同步至青龙`);
} else {
// create
const addUrl = `${URL}/open/envs`;
await $.http.post({
url: addUrl,
headers: {
'Authorization': `Bearer ${qlToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify([{
name: ENV_NAME,
value: newValue,
remarks: "自动抓包创建"
}])
});
$.msg(SCRIPT_NAME, `✅ 推送成功 [新建]`, `UID: ${accountToPush.uid.slice(0, 6)}... 已创建青龙变量`);
}
} 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),
put: (opts) => that.sendReq('PUT', opts),
delete: (opts) => that.sendReq('DELETE', 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 : (method === 'PUT' ? $httpClient.put : $httpClient.get);
client(reqOpts, (err, res, body) => {
if (err) reject(err);
else resolve({ body: body, headers: res.headers });
});
} else {
reject("Env not supported");
}
});
}
}(name);
}