添加 js/nfsq.js

This commit is contained in:
chickliu 2025-12-08 18:01:25 +08:00
parent 747176c1cd
commit 9633368cc7

504
js/nfsq.js Normal file
View 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);
}