|
|

楼主 |
发表于 昨天 05:01
高大上手机用户
|
显示全部楼层
陕西省咸阳市
const crypto = require('crypto')
const https = require('https')
const http = require('http')
const zlib = require('zlib')
// ============ 配置 ============
// 接口地址,可通过环境变量覆盖
const API_BASE = process.env.TREECOIN_API_BASE || 'https://treecoin.cn/api'
const AUTH_CODES = (process.env.TREECOIN_AUTH_CODE || '').split('&').filter(Boolean)
// 可选:固定设备指纹(抓包获取可降低风控,通过环境变量 TREECOIN_DEVICE_FP 配置)
const FIXED_DEVICE_FP = process.env.TREECOIN_DEVICE_FP || 'BROWSER_1783593257377_qrbf23j6i'
// 加密算法
const ALGORITHM = 'aes-256-cbc'
// ============ 工具函数 ============
/**
* AES-CBC 加密
*/
function aesEncrypt(text, sessionKeyBase64, nonceUuid) {
const key = Buffer.from(sessionKeyBase64, 'base64')
const iv = Buffer.from(nonceUuid.replace(/-/g, ''), 'hex')
const cipher = crypto.createCipheriv(ALGORITHM, key, iv)
let encrypted = cipher.update(text, 'utf8', 'base64')
encrypted += cipher.final('base64')
return encrypted
}
/**
* AES-CBC 解密
*/
function aesDecrypt(cipherBase64, sessionKeyBase64, nonceUuid) {
const key = Buffer.from(sessionKeyBase64, 'base64')
const iv = Buffer.from(nonceUuid.replace(/-/g, ''), 'hex')
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)
let decrypted = decipher.update(cipherBase64, 'base64', 'utf8')
decrypted += decipher.final('utf8')
return JSON.parse(decrypted)
}
/**
* 发送 HTTP 请求
*/
function request(path, method, body, deviceFp) {
return new Promise((resolve, reject) => {
const url = new URL(API_BASE + path)
const isHttps = url.protocol === 'https:'
const lib = isHttps ? https : http
const data = body ? JSON.stringify(body) : null
const options = {
hostname: url.hostname,
port: url.port || (isHttps ? 443 : 80),
path: url.pathname,
method: method,
headers: {
'Content-Type': 'application/json',
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate',
'Origin': 'https://treecoin.cn',
'Referer': 'https://treecoin.cn/home',
'User-Agent': 'Mozilla/5.0 (Linux; Android 14; 24069RA21C Build/UKQ1.240116.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/146.0.7680.178 Mobile Safari/537.36 XWEB/1460217 MMWEBSDK/20260202 MicroMessenger/8.0.71.3080 WeChat/arm64 NetType/4G Language/zh_CN',
'X-Requested-With': 'com.tencent.mm'
}
}
if (data) {
options.headers['Content-Length'] = Buffer.byteLength(data)
}
if (deviceFp) {
options.headers['X-Device-Fingerprint'] = deviceFp
}
const req = lib.request(options, (res) => {
// 自动处理压缩响应
let stream = res
const encoding = res.headers['content-encoding']
if (encoding === 'gzip') stream = res.pipe(zlib.createGunzip())
else if (encoding === 'deflate') stream = res.pipe(zlib.createInflate())
let chunks = ''
stream.on('data', (chunk) => { chunks += chunk })
stream.on('end', () => {
try {
resolve(JSON.parse(chunks))
} catch (e) {
resolve({ c: 0, msg: '响应解析失败', raw: chunks })
}
})
})
req.on('error', reject)
req.setTimeout(15000, () => {
req.destroy(new Error('请求超时'))
})
if (data) req.write(data)
req.end()
})
}
/**
* 生成符合平台规则的设备指纹
*/
function generateDeviceId() {
const ts = Date.now()
const random = Math.random().toString(36).substring(2, 12)
return `BROWSER_${ts}_${random}`
}
/**
* 控制台输出带时间戳
*/
function log(msg) {
const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })
console.log(`[${now}] ${msg}`)
}
/**
* 延迟
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
// ============ 核心逻辑 ============
/**
* 使用授权码登录
*/
async function loginWithAuthCode(authCode) {
const deviceFingerprint = FIXED_DEVICE_FP || generateDeviceId()
const result = await request('/auth/login-by-auth-code', 'POST', {
authCode,
device_fingerprint: deviceFingerprint
}, deviceFingerprint)
if (result.c !== 1 || !result.data) {
throw new Error(result.msg || '登录失败')
}
const userInfo = (result.data.user && result.data.user.dataValues) || result.data.user || {}
const session = result.data.session || {}
return {
token: result.data.token,
sessionId: session.sessionId,
sessionKey: session.sessionKey,
openid: result.data.openid,
deviceFingerprint: deviceFingerprint,
userInfo: userInfo
}
}
/**
* 前置会话校验(签到必走步骤)
*/
async function preCheck(sessionId, sessionKey, deviceFp) {
const nonce = crypto.randomUUID()
const timestamp = Date.now()
const payload = JSON.stringify({ timestamp })
const encryptedData = aesEncrypt(payload, sessionKey, nonce)
return request('/app/t', 'POST', {
sessionId,
encryptedData,
nonce,
timestamp
}, deviceFp)
}
/**
* 执行签到
*/
async function doSignin(sessionId, sessionKey, deviceId, token) {
const nonce = crypto.randomUUID()
const timestamp = Date.now()
// 加密请求数据
const payload = JSON.stringify({ timestamp, token })
const encryptedData = aesEncrypt(payload, sessionKey, nonce)
const result = await request('/app/signin', 'POST', {
sessionId,
encryptedData,
nonce,
timestamp
}, deviceId)
// 解密加密响应
if (result.encrypted && result.data) {
return aesDecrypt(result.data, sessionKey, nonce)
}
return result
}
/**
* 单账号签到流程
*/
async function signinForAccount(authCode, index) {
log(`---------- 账号 ${index + 1} ----------`)
log(`授权码: ${authCode.substring(0, 8)}****`)
try {
// 1. 登录
log('正在登录...')
const loginResult = await loginWithAuthCode(authCode)
log(`登录成功!用户: ${loginResult.userInfo.nickName || '未知'} (openid: ${loginResult.openid.substring(0, 8)}...)`)
const { token, sessionId, sessionKey, deviceFingerprint } = loginResult
// 2. 设置签到前置标记
log('设置签到前置标记...')
await preCheck(sessionId, sessionKey, deviceFingerprint)
await sleep(350)
// 3. 签到
log('执行签到...')
const signinResult = await doSignin(sessionId, sessionKey, deviceFingerprint, token)
if (signinResult.c === 1) {
const data = signinResult.data || {}
log('========================================')
log(' 签到成功!')
log('========================================')
log(` 连续签到天数: ${data.continuousDays || 1} 天`)
log(` 当前持有树苗: ${data.vitality || '未知'}`)
log(` 本次树苗奖励: +${data.baseReward || 0}`)
if (data.continuousReward && data.continuousReward > 0) {
log(` 连续签到奖励: +${data.continuousReward} (满7天额外奖励!)`)
}
log(` 本次总收益: +${data.increase || 0} 树苗`)
log('========================================')
log('提示: 大树(能量币)收益将在签到后异步发放')
log(' 持有树苗越多,每日大树产币越多!')
return { success: true, data }
} else {
log(`签到结果: ${signinResult.msg || '未知'}`)
// 今日已签到也算成功
if (signinResult.msg && signinResult.msg.includes('已签到')) {
log('今天已经签到过了,明天再来吧!')
return { success: true, alreadySigned: true }
}
return { success: false, msg: signinResult.msg }
}
} catch (error) {
log(`签到失败: ${error.message}`)
return { success: false, msg: error.message }
}
}
/**
* 主函数
*/
async function main() {
console.log('')
console.log('╔══════════════════════════════════════════╗')
console.log('║ 绿树田园 - 每日签到脚本 (青龙版) ║')
console.log('║ ║')
console.log('║ 种树赚钱,每日签到,连续7天额外奖励! ║')
console.log('║ 树苗可兑换真实树苗,为地球添一份绿! ║')
console.log('║ 当前币价:3.12,自由交易 ║')
console.log('╚══════════════════════════════════════════╝')
console.log('')
if (AUTH_CODES.length === 0) {
console.log('❌ 未配置授权码!')
console.log('')
console.log('请设置环境变量 TREECOIN_AUTH_CODE')
console.log('获取方式:VX小程序「绿树田园」→ 我的 → 授权码')
console.log('多账号用 & 分隔')
console.log('')
console.log('青龙面板配置:')
console.log(' 环境变量 → 添加 → TREECOIN_AUTH_CODE = 你的授权码')
console.log(' 定时任务 → task treecoin-qinglong-signin.js')
console.log(' cron表达式: 0 8 * * * (每天8点)')
return
}
log(`共 ${AUTH_CODES.length} 个账号待签到`)
log(`API地址: ${API_BASE}`)
console.log('')
let successCount = 0
let failCount = 0
for (let i = 0; i < AUTH_CODES.length; i++) {
const result = await signinForAccount(AUTH_CODES[i].trim(), i)
if (result.success) {
successCount++
} else {
failCount++
}
// 多账号间隔10秒,降低风控风险
if (i < AUTH_CODES.length - 1) {
log('等待10秒后处理下一个账号...')
await sleep(10000)
}
}
console.log('')
log('========== 签到汇总 ==========')
log(`成功: ${successCount} 个账号`)
log(`失败: ${failCount} 个账号`)
log('==============================')
if (failCount > 0) {
log('部分账号签到失败,请检查授权码是否正确或是否已过期')
}
}
// 运行
main().catch(err => {
log(`脚本运行异常: ${err.message}`)
console.error(err)
}) |
|