在您安装前,Greasy Fork 希望您知道此脚本声明其包含了一些负面功能。这些功能也许会使脚本作者获利,而不能给您带来任何直接的金钱收益。
此脚本含有追踪您的操作的代码。
脚本作者的说明:
本脚本会收集用户ID、用户名等个人标识信息,并上报至作者服务器,用于防止助力码滥用和统计使用情况。用户需在弹窗中明确同意后方可启用。
在抽奖页面显示用户信息并支持复制功能;脚本会收集并上报用户ID、用户名、VIP等级等数据,用于统计与防止滥用,请在弹窗中明确同意后再使用。
// ==UserScript==
// @name 115 助力工具
// @namespace http://tampermonkey.net/
// @version 2.1.0
// @description 在抽奖页面显示用户信息并支持复制功能;脚本会收集并上报用户ID、用户名、VIP等级等数据,用于统计与防止滥用,请在弹窗中明确同意后再使用。
// @author allen666 (原作者), zsc (修改者)
// @match https://f.115.com/social/games/lucky5*
// @match https://act.115.com/api/1.0/web/1.0/invite_boost/invite_list*
// @match https://passportapi.115.com/app/1.0/web/26.0/user/base_info*
// @run-at document-start
// @grant unsafeWindow
// @grant GM_xmlhttpRequest
// @connect 115.xiaocai.site
// @license MIT
// @antifeature tracking 本脚本会收集用户ID、用户名等个人标识信息,并上报至作者服务器,用于防止助力码滥用和统计使用情况。用户需在弹窗中明确同意后方可启用。
// ==/UserScript==
;(async function () {
'use strict'
if (window.__CODE115_SCRIPT_RUNNING__) {
console.warn('[115助力工具] 已检测到脚本实例,跳过重复执行')
return
}
window.__CODE115_SCRIPT_RUNNING__ = true
// 强制隐私同意:用户同意前不继续执行脚本
async function requireConsent() {
const storageKey = 'code115_privacy_consent_v1'
try {
if (window.localStorage && localStorage.getItem(storageKey) === '1') {
return true
}
} catch (e) {
// 忽略 localStorage 访问错误,继续展示弹窗
console.warn('localStorage 不可用或被阻止', e)
}
// 等待 DOM 可用
if (document.readyState === 'loading') {
await new Promise((resolve) => document.addEventListener('DOMContentLoaded', resolve, { once: true }))
}
return await new Promise((resolve) => {
// 创建遮罩和弹窗
const overlay = document.createElement('div')
overlay.id = 'code115-consent-overlay'
Object.assign(overlay.style, {
position: 'fixed',
left: '0',
top: '0',
right: '0',
bottom: '0',
background: 'rgba(0,0,0,0.6)',
zIndex: '2147483647',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
})
const box = document.createElement('div')
Object.assign(box.style, {
width: '720px',
maxWidth: '95%',
maxHeight: '80%',
overflowY: 'auto',
background: '#fff',
borderRadius: '8px',
padding: '18px',
boxSizing: 'border-box',
fontSize: '14px',
color: '#222',
})
box.innerHTML = `
<h2 style="margin:0 0 8px 0;font-size:18px">隐私条款与数据上报声明</h2>
<div style="font-size:13px;color:#333;line-height:1.5;margin-bottom:12px;">
使用 【115 助力工具】 脚本前请阅读并同意以下条款:
<ul style="margin:6px 0 6px 20px;padding:0;">
<li>脚本会读取部分公开的用户信息(如用户ID、助力码、助力记录、用户vip等级等, 不含Cookies)用于在界面展示和上报服务器,以便统计与调试。</li>
<li>上报的数据仅用于该工具的功能实现与使用统计,不会用于其他任何商业目的. </li>
<li>为什么要收集这些信息? 主要是防止助力码滥用,助力失效以及恶意上传无效的助力码。</li>
<li>本脚本收集的数据,完全公开,任何人均可访问查看,查看地址Link: <a href="https://115.xiaocai.site/codes" target="_blank" rel="noopener">https://115.xiaocai.site/codes</a></li>
<li>请确保不要将敏感信息放入助力码输入框;若不同意以上条款,请点击“不同意并退出”,脚本将停止运行。</li>
</ul>
详细说明请参见脚本仓库或作者提供的文档。
</div>
<label style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
<input type="checkbox" id="code115-consent-checkbox" /> 我已阅读并同意上述隐私条款与数据上报
</label>
<div style="display:flex;gap:8px;justify-content:flex-end;">
<button id="code115-consent-decline" style="background:#dc3545;color:#fff;border:none;padding:8px 12px;border-radius:4px;cursor:pointer;">不同意并退出(请手动卸载)</button>
<button id="code115-consent-agree" disabled style="background:#28a745;color:#fff;border:none;padding:8px 12px;border-radius:4px;cursor:pointer;">同意并继续(同意以后,不再弹出)</button>
</div>
`
overlay.appendChild(box)
document.body.appendChild(overlay)
const checkbox = document.getElementById('code115-consent-checkbox')
const agreeBtn = document.getElementById('code115-consent-agree')
const declineBtn = document.getElementById('code115-consent-decline')
checkbox.addEventListener('change', () => {
agreeBtn.disabled = !checkbox.checked
})
function cleanUp() {
try {
overlay.remove()
} catch (e) {
// ignore
}
}
agreeBtn.addEventListener('click', () => {
try {
localStorage.setItem(storageKey, '1')
} catch (e) {
console.warn('无法写入 localStorage', e)
}
cleanUp()
resolve(true)
})
declineBtn.addEventListener('click', () => {
cleanUp()
// 给用户一个短提示,然后停止脚本
try {
const tip = document.createElement('div')
tip.style.position = 'fixed'
tip.style.left = '50%'
tip.style.top = '20%'
tip.style.transform = 'translateX(-50%)'
tip.style.background = '#fff'
tip.style.padding = '12px 18px'
tip.style.borderRadius = '6px'
tip.style.zIndex = '2147483647'
tip.style.boxShadow = '0 6px 20px rgba(0,0,0,0.2)'
tip.textContent = '您已选择不同意隐私条款,脚本将停止运行。'
document.body.appendChild(tip)
setTimeout(() => tip.remove(), 3500)
} catch (e) {
/* ignore */
}
resolve(false)
})
})
}
const consent = await requireConsent()
if (!consent) {
console.warn('用户未同意隐私条款,脚本停止执行')
return
}
let isRunning = false
let controller = new AbortController()
let startTime = null
let completedRequests = 0
let isMinimized = false
let baseInfoReady = false
const CONFIG = {
API_URL: 'https://115.xiaocai.site',
DEBUG: false, // 是否开启调试日志
}
const USERINFO = {
user_id: 0,
user_name: '',
vip: '',
size_used: '',
size_total: '',
expire: 0,
boost_code: '',
cycle_index: 0,
boost_count_limit: 0,
exchange_limit: 0,
cycle_boost_count_used: 0,
cycle_boost_value: 0,
cycle_rewards_earned: 0,
can_boost: false,
can_exchange: false,
total_rewards_exchanged: 0,
total_boost_earned: 0,
total_users_boosted: 0,
total_relationships_established: 0,
updated_at: Math.floor(Date.now() / 1000),
}
const BASEINFO = {
user_id: 0,
user_name: '',
vip: '',
size_total: '',
size_used: '',
expire: 0,
}
const BOOSTINFO = {
user_id: 0,
user_boost_code: '',
invitee_state: -1,
invitee_code: -1,
invitee_boost_code: '',
invitee_id: null,
invitee_name: null,
invitee_exceed_boost: null,
updated_at: 0,
}
const USERINFO_DEFAULT = { ...USERINFO }
const BASEINFO_DEFAULT = { ...BASEINFO }
const BOOSTINFO_DEFAULT = { ...BOOSTINFO }
// 防止重复加载
if (document.getElementById('boost-panel')) return
// ✅ 严格限定:只在抽奖页面显示
if (!window.location.href.includes('https://f.115.com/social/games/lucky5')) return
// 新增拦截请求 --- 获取助力记录 (暂时只获取一次,除非用户自动点击下拉加载更多)
const originFetch = fetch
window.unsafeWindow.fetch = function (...args) {
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url
if (!url || (!url.includes('invite_boost/invite_list') && !url.includes('user/base_info'))) {
// 不是我们关心的 URL,直接返回原始 fetch
return originFetch.apply(this, args)
}
// 只拦截关心的 URL
return originFetch.apply(this, args).then(async (response) => {
try {
const data = await response.clone().json()
if (url.includes('invite_boost/invite_list')) {
if (USERINFO?.user_id && USERINFO?.boost_code && data?.data?.list?.length) {
const invitee_ids = data.data.list.map((item) => item.invitee_id)
GM_xmlhttpRequest({
method: 'POST',
url: `${CONFIG.API_URL}/invite_list2`,
data: JSON.stringify({ invitee_ids }),
headers: { 'Content-Type': 'application/json' },
timeout: 10000,
})
}
} else if (url.includes('user/base_info') && data.state === 1 && data.code == 0) {
Object.assign(BASEINFO, {
user_id: data.data.user_id,
user_name: data.data.user_name,
vip: data.data.vip,
size_total: data.data.size_total,
expire: data.data.expire,
})
baseInfoReady = true
}
} catch (err) {
console.error('解析 fetch 响应失败:', err)
}
return response
})
}
function logUserInfo(userInfo) {
// 直接异步上报,不处理回调
const url0 = `${CONFIG.API_URL}/upload_code`
GM_xmlhttpRequest({
method: 'POST',
url: url0,
data: userInfo ? JSON.stringify(userInfo) : JSON.stringify(USERINFO),
headers: {
'Content-Type': 'application/json',
},
// 可选:添加超时避免长时间挂起
timeout: 5000,
onload: function (response) {
// 静默处理成功响应,避免过多console输出
console.log('[logUserInfo] 上报服务器响应:', response.responseText)
},
onerror: function (error) {
// 可选:只在调试时显示错误
if (CONFIG.DEBUG) {
console.log('上报服务器异常:', error)
}
},
})
}
function submitBoostInfo(boostData) {
const DEBUG = CONFIG.DEBUG
GM_xmlhttpRequest({
method: 'POST',
url: `${CONFIG.API_URL}/boost_upload`,
data: JSON.stringify(boostData),
headers: {
'Content-Type': 'application/json',
},
timeout: 3000,
onload: function (response) {
if (DEBUG) {
try {
const data = JSON.parse(response.responseText)
console.log('[submitBoostInfo] 服务器响应:', data)
} catch (e) {
console.log('[submitBoostInfo] 响应解析失败')
}
}
},
onerror: function (error) {
if (DEBUG) {
console.log('[submitBoostInfo] 网络错误')
}
},
ontimeout: function () {
if (DEBUG) {
console.log('[submitBoostInfo] 请求超时')
}
},
})
}
// 创建侧边栏控制按钮(可拖动)
const createToggleButton = () => {
const btn = document.createElement('button')
btn.id = 'boost-toggle-btn'
Object.assign(btn.style, {
position: 'fixed',
top: '200px',
right: '0',
width: '80px',
height: '60px', // 增加高度
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px 0 0 4px',
cursor: 'move',
zIndex: '9999',
fontSize: '14px',
boxShadow: '0 2px 6px rgba(0,0,0,0.2)',
textAlign: 'center',
lineHeight: '1.3',
padding: '8px 0',
})
btn.innerHTML =
'助力工具<br><span style="font-size:10px;font-style:italic;">by zsc</span><br><span style="font-size:10px;display:block;">v2.1</span>'
let isDragging = false
let offsetX, offsetY
btn.addEventListener('mousedown', (e) => {
if (e.target.tagName !== 'BUTTON') return
isDragging = true
offsetX = e.clientX - parseInt(btn.style.right || '0')
offsetY = e.clientY - parseInt(btn.style.top || '200px')
e.preventDefault()
})
document.addEventListener('mousemove', (e) => {
if (!isDragging) return
const right = window.innerWidth - (e.clientX + offsetX)
btn.style.top = `${e.clientY - offsetY}px`
btn.style.right = `${Math.max(right, 0)}px`
})
document.addEventListener('mouseup', () => {
isDragging = false
})
btn.addEventListener('click', (e) => {
if (e.target.tagName === 'BUTTON' || e.target.parentElement.tagName === 'BUTTON') {
togglePanel()
}
})
return btn
}
// 创建主面板
const createPanel = () => {
const panel = document.createElement('div')
panel.id = 'boost-panel'
Object.assign(panel.style, {
position: 'fixed',
top: '120px',
right: '-320px',
width: '300px',
height: '680px', // 增加面板高度
backgroundColor: 'white',
border: '1px solid #ddd',
borderRadius: '8px 0 0 8px',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
zIndex: '9999',
transition: 'right 0.3s ease, height 0.3s ease',
overflow: 'hidden',
fontFamily: 'Arial, sans-serif',
})
panel.innerHTML = `
<div id="panel-header"
style="padding: 12px; background: #007bff; color: white; font-weight: bold; cursor: move; display: flex; justify-content: space-between; align-items: center;">
<div style="line-height: 1.4;">
<div>115 助力工具</div>
</div>
<div style="display: flex; gap: 10px;">
<button id="minimize-btn" style="background:none;border:none;color:white;font-size:16px;cursor:pointer;">−</button>
<button id="close-btn" style="background:none;border:none;color:white;font-size:16px;cursor:pointer;">×</button>
</div>
</div>
<div id="panel-content" style="padding: 16px; display: block;">
<!-- 用户信息 -->
<div id="user-info" style="margin-bottom:12px;font-size:12px;line-height:1.5;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px;">
<span>用户 ID:</span>
<div style="display:flex;align-items:center;gap:4px;">
<span id="user-id">获取中...</span>
<button id="copy-user-id" class="copy-btn"
style="background:#eee;border:none;width:24px;height:20px;font-size:10px;cursor:pointer;border-radius:2px;">
复制
</button>
</div>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px;">
<span>我的助力码:</span>
<div style="display:flex;align-items:center;gap:4px;">
<span id="my-boost-code">获取中...</span>
<button id="copy-boost-code" class="copy-btn"
style="background:#eee;border:none;width:24px;height:20px;font-size:10px;cursor:pointer;border-radius:2px;">
复制
</button>
</div>
</div>
</div>
<label style="display:block;margin-bottom:8px;font-size:14px;">助力码列表(每行一个)</label>
<a href="https://115.xiaocai.site/summary" target="_blank" rel="noopener"
style="font-size:12px;color:#007bff;text-decoration:underline;margin-bottom:4px;display:inline-block;">
查看用户中奖概率
</a>
|
<a href="https://115.xiaocai.site/views" target="_blank" rel="noopener"
style="font-size:12px;color:#007bff;text-decoration:underline;margin-bottom:4px;display:inline-block;">
查看幸运值走势图
</a>
<br/>
<a href="https://115.xiaocai.site/codes" target="_blank" rel="noopener"
style="font-size:12px;color:#007bff;text-decoration:underline;margin-bottom:4px;display:inline-block;">
查看可用的助力码
</a>
|
<a href="https://115.xiaocai.site/boost" target="_blank" rel="noopener"
style="font-size:12px;color:#007bff;text-decoration:underline;margin-bottom:4px;display:inline-block;">
查看已使用助力码
</a>
<br/>
<a href="https://docs.qq.com/form/page/DZWJ3ZE9qakVndUpu" target="_blank" rel="noopener"
style="font-size:12px;color:#007bff;text-decoration:underline;margin-bottom:4px;display:inline-block;">
查看已无效助力码
</a>
|
<a href="https://docs.qq.com/doc/DZUNFV0JsT2F4QUdp" target="_blank" rel="noopener"
style="font-size:12px;color:#007bff;text-decoration:underline;margin-bottom:4px;display:inline-block;">
查看助力规则说明
</a>
<textarea id="boost-codes" rows="6"
style="width:100%;font-family:monospace;font-size:12px;padding:8px;
border:1px solid #ccc;border-radius:4px;resize:none;"
placeholder="ABC123 XYZ789"></textarea>
<div style="margin-top:4px;color:red;font-size:12px;" id="boost-limit-tip"></div>
<div id="action-buttons" style="margin-top:8px;display:flex;gap:8px;">
<button id="start-boost"
style="flex:1;background:#28a745;color:white;
border:none;padding:10px 0;border-radius:4px;font-size:14px;
cursor:pointer;">开始助力</button>
</div>
<div id="stats" style="margin-top:12px;font-size:12px;">
<div>总数: <span id="total">0</span></div>
<div style="color:green;">成功: <span id="success">0</span></div>
<div style="color:orange;">重复: <span id="duplicate">0</span></div>
<div style="color:#666;">速率: <span id="rate">0</span> req/s</div>
</div>
<div style="margin-top:16px;font-size:14px;font-weight:bold;">执行日志</div>
<div id="log-area"
style="height:200px;overflow-y:auto;border:1px solid #eee;
padding:8px;background:#f9f9f9;font-size:12px;">
<div class="log-item" style="color:#666;">等待启动...</div>
</div>
<!-- 加载动画 -->
<div id="loading" style="display:none;text-align:center;margin-top:8px;">
<div style="display:inline-block;width:16px;height:16px;border:2px solid #ddd;border-top-color:#007bff;border-radius:50%;animation:spin 1s linear infinite;"></div>
<span style="margin-left:8px;font-size:12px;color:#666;">处理中...</span>
</div>
</div>
<style>
@keyframes spin {
to { transform: rotate(360deg); }
}
.copy-success::after {
content: ' ✓';
color: green;
animation: fadeOut 1.5s;
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
</style>
`
return panel
}
// 添加日志
function addLog(message, color = 'black') {
const logArea = document.getElementById('log-area')
const item = document.createElement('div')
item.className = 'log-item'
item.style.color = color
item.style.margin = '4px 0'
item.style.whiteSpace = 'nowrap'
item.style.overflow = 'hidden'
item.style.textOverflow = 'ellipsis'
const time = new Date().toLocaleTimeString()
item.textContent = `[${time}] ${message}`
logArea.appendChild(item)
requestAnimationFrame(() => {
logArea.scrollTop = logArea.scrollHeight
})
}
// 更新统计
function updateStats(key) {
const el = document.getElementById(key)
const val = parseInt(el.textContent || '0')
el.textContent = val + 1
}
// 重置统计
function resetStats() {
document.getElementById('success').textContent = '0'
document.getElementById('duplicate').textContent = '0'
document.getElementById('rate').textContent = '0'
}
// 更新速率
function updateRate() {
if (!startTime) return
const elapsed = (Date.now() - startTime) / 1000
const rate = elapsed > 0 ? (completedRequests / elapsed).toFixed(1) : '0'
document.getElementById('rate').textContent = rate
}
// 获取用户信息
async function fetchUserInfo() {
try {
const response = await fetch(`https://act.115.com/api/1.0/web/1.0/invite_boost/user_info?_t=${Date.now()}`, {
method: 'GET',
credentials: 'include',
})
if (!response.ok) throw new Error('网络错误')
const data = await response.json()
if (data.state === 1) {
const userInfo = data.data.user_info
const stats = data.data.stats
// 更新全局用户信息
USERINFO.user_id = data.data.user_info.user_id
USERINFO.boost_code = data.data.user_info.boost_code
USERINFO.cycle_index = data.data.cycle_info.cycle_index
USERINFO.boost_count_limit = data.data.stats.boost_count_limit
USERINFO.exchange_limit = data.data.stats.exchange_limit
USERINFO.cycle_boost_count_used = data.data.stats.cycle_boost_count_used
USERINFO.cycle_boost_value = data.data.stats.cycle_boost_value
USERINFO.cycle_rewards_earned = data.data.stats.cycle_rewards_earned
USERINFO.can_boost = data.data.stats.can_boost
USERINFO.can_exchange = data.data.stats.can_exchange
USERINFO.total_rewards_exchanged = data.data.stats.total_rewards_exchanged
USERINFO.total_boost_earned = data.data.stats.total_boost_earned
USERINFO.total_users_boosted = data.data.stats.total_users_boosted
USERINFO.total_relationships_established = data.data.stats.total_relationships_established
USERINFO.updated_at = Math.floor(Date.now() / 1000)
if (String(BASEINFO.user_id) != String(USERINFO.user_id) || String(USERINFO.user_id) == '0') {
console.warn('用户ID不匹配,可能未登录或获取失败')
// 清空用户信息
USERINFO = structuredClone(USERINFO_DEFAULT)
BASEINFO = structuredClone(BASEINFO_DEFAULT)
return null
}
// if (!userInfo.user_id || userInfo.user_id === 0) {
// console.warn('用户ID获取失败')
// return null
// }
console.log('[fetchUserInfo] 获取到用户信息')
USERINFO.user_name = BASEINFO.user_name
USERINFO.vip = BASEINFO.vip
USERINFO.size_used = BASEINFO.size_used
USERINFO.size_total = BASEINFO.size_total
USERINFO.expire = BASEINFO.expire
BOOSTINFO.user_id = USERINFO.user_id
BOOSTINFO.user_boost_code = USERINFO.boost_code
// 拷贝一份
const userInfoCopy = structuredClone(USERINFO)
logUserInfo(userInfoCopy) // 上报用户信息
// 更新用户信息
document.getElementById('user-id').textContent = userInfoCopy.user_id
document.getElementById('my-boost-code').textContent = userInfoCopy.boost_code
// 控制开始助力按钮
const startBtn = document.getElementById('start-boost')
const tipEl = document.getElementById('boost-limit-tip')
if (!stats.can_boost) {
startBtn.disabled = true
startBtn.style.opacity = '0.6'
startBtn.style.cursor = 'not-allowed'
tipEl.textContent = '当前助力次数已用完'
} else {
startBtn.disabled = false
startBtn.style.opacity = '1'
startBtn.style.cursor = 'pointer'
tipEl.textContent = ''
}
addLog('✅ 用户信息获取成功', 'green')
return data
} else {
addLog(`❌ 获取用户信息失败: ${data.message}`, 'red')
return null
}
} catch (err) {
addLog('❌ 网络错误,无法获取用户信息', 'red')
console.error(err)
return null
}
}
// 复制到剪贴板(兼容性增强版)
function copyToClipboard(text, button, successText = '已复制') {
// 创建临时 textarea 元素用于复制
const tempTextarea = document.createElement('textarea')
tempTextarea.value = text
tempTextarea.setAttribute('readonly', '')
Object.assign(tempTextarea.style, {
position: 'absolute',
left: '-9999px',
opacity: 0,
width: '1px',
height: '1px',
})
document.body.appendChild(tempTextarea)
// 尝试使用现代 Clipboard API
if (navigator.clipboard) {
navigator.clipboard
.writeText(text)
.then(() => {
showCopyFeedback(button, successText)
})
.catch((err) => {
console.warn('Clipboard API 失败,回退到 execCommand:', err)
fallbackCopy(tempTextarea, button, successText)
})
} else {
// 浏览器不支持 navigator.clipboard
fallbackCopy(tempTextarea, button, successText)
}
// 移除临时元素
setTimeout(() => {
document.body.removeChild(tempTextarea)
}, 1000)
}
// 回退方案:使用 document.execCommand
function fallbackCopy(tempTextarea, button, successText) {
try {
tempTextarea.select()
tempTextarea.setSelectionRange(0, 99999) // 兼容移动端
const successful = document.execCommand('copy')
if (successful) {
showCopyFeedback(button, successText)
} else {
throw new Error('execCommand failed')
}
} catch (err) {
console.error('复制失败:', err)
alert('复制失败,请长按选择并复制')
}
}
// 显示复制成功反馈
function showCopyFeedback(button, successText) {
const originalText = button.textContent
button.textContent = successText
button.classList.add('copy-success')
setTimeout(() => {
button.textContent = originalText
button.classList.remove('copy-success')
}, 1500)
}
// 发送助力请求(带重试机制)
async function sendBoost(code, retryCount = 3) {
for (let i = 0; i < retryCount; i++) {
try {
const formData = new FormData()
formData.append('boost_code', code)
formData.append('source', 'link')
const response = await fetch('https://act.115.com/api/1.0/web/1.0/invite_boost/accept_invite', {
method: 'POST',
body: formData,
credentials: 'include',
signal: controller.signal,
})
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const data = await response.json()
return data
} catch (err) {
if (err.name === 'AbortError') return { state: 0, message: '请求被取消' }
if (i === retryCount - 1) {
return { state: 0, message: `网络错误(已重试${retryCount}次)` }
}
await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, i)))
}
}
}
// 主要逻辑
async function startBoost() {
if (isRunning) return
const textarea = document.getElementById('boost-codes')
const codes = textarea.value
.split('\n')
.map((line) => line.trim().toUpperCase())
.filter((line) => /^[A-Z0-9]{6}$/.test(line))
if (codes.length === 0) {
alert('请输入有效的6位助力码(A-Z, 0-9),每行一个')
return
}
// 再次检查是否可助力
const stats = await fetchUserInfo()
if (!stats?.data?.stats?.can_boost) {
alert('当前助力次数已用完,无法继续助力')
return
}
isRunning = true
controller = new AbortController()
startTime = Date.now()
completedRequests = 0
// 冻结输入框和原按钮
textarea.disabled = true
const startBtn = document.getElementById('start-boost')
if (startBtn) startBtn.style.display = 'none'
// 显示加载动画
document.getElementById('loading').style.display = 'block'
// 清除旧的按钮
const actionButtons = document.getElementById('action-buttons')
const existingStop = document.getElementById('stop-boost')
if (existingStop) existingStop.remove()
// 添加“停止”按钮
const stopBtn = document.createElement('button')
stopBtn.id = 'stop-boost'
stopBtn.textContent = '停止助力'
stopBtn.style = 'flex:1;background:#dc3545;color:white;border:none;padding:10px 0;border-radius:4px;font-size:14px;cursor:pointer;'
stopBtn.onclick = () => {
isRunning = false
controller.abort()
addLog('🛑 用户手动停止助力', 'red')
finishProcess()
}
actionButtons.appendChild(stopBtn)
// 重置并显示总数
resetStats()
document.getElementById('total').textContent = codes.length
// 清空日志
document.getElementById('log-area').innerHTML = ''
addLog(`共发现 ${codes.length} 个有效助力码,开始处理...`, 'blue')
// 逐个处理
for (const code of codes) {
if (!isRunning) break
if (code === USERINFO.boost_code) {
addLog(`跳过自己的助力码: ${code}`, '#666')
continue
}
addLog(`正在助力: ${code}`, '#007bff')
const result = await sendBoost(code)
BOOSTINFO.invitee_state = result.state
BOOSTINFO.invitee_code = result.code
BOOSTINFO.invitee_boost_code = code
BOOSTINFO.invitee_id = result.data?.inviter_id || null
BOOSTINFO.invitee_name = result.data?.inviter_name || null
BOOSTINFO.invitee_exceed_boost = result.data?.exceed_boost || null
BOOSTINFO.updated_at = Math.floor(Date.now() / 1000)
if (result.state === 1) {
submitBoostInfo(structuredClone(BOOSTINFO))
addLog(`✅ 成功助力: ${result.data.inviter_name || '未知用户'}`, 'green')
updateStats('success')
} else if (result.code === 40203004 || result.message.includes('已经')) {
submitBoostInfo(structuredClone(BOOSTINFO))
addLog(`🟡 已助力过: ${code}`, 'orange')
updateStats('duplicate')
} else if (result.code === 40203002 || result.message.includes('无效')) {
submitBoostInfo(structuredClone(BOOSTINFO))
console.log('助力码无效:', BOOSTINFO)
addLog(`❌ 无效助力码: ${code}`, 'red')
} else {
addLog(`❌ 助力失败: ${result.message || '未知错误'}`, 'red')
}
completedRequests++
updateRate()
await new Promise((resolve) => {
if (!isRunning) return resolve()
setTimeout(resolve, 800)
})
}
finishProcess()
}
function finishProcess() {
isRunning = false
const stopBtn = document.getElementById('stop-boost')
if (stopBtn) stopBtn.remove()
document.getElementById('loading').style.display = 'none'
const actionButtons = document.getElementById('action-buttons')
actionButtons.innerHTML = ''
const clearBtn = document.createElement('button')
clearBtn.textContent = '清空'
clearBtn.style = 'flex:1;background:#6c757d;color:white;border:none;padding:10px 0;border-radius:4px;font-size:14px;cursor:pointer;'
clearBtn.onclick = clearAll
const saveBtn = document.createElement('button')
saveBtn.textContent = '保存日志'
saveBtn.style = 'flex:1;background:#17a2b8;color:white;border:none;padding:10px 0;border-radius:4px;font-size:14px;cursor:pointer;'
saveBtn.onclick = saveLog
actionButtons.appendChild(clearBtn)
actionButtons.appendChild(saveBtn)
}
function clearAll() {
const textarea = document.getElementById('boost-codes')
textarea.value = ''
textarea.disabled = false
const logArea = document.getElementById('log-area')
logArea.innerHTML = '<div class="log-item" style="color:#666;">等待启动...</div>'
document.getElementById('total').textContent = '0'
resetStats()
const actionButtons = document.getElementById('action-buttons')
actionButtons.innerHTML = `
<button id="start-boost"
style="flex:1;background:#28a745;color:white;
border:none;padding:10px 0;border-radius:4px;font-size:14px;
cursor:pointer;">开始助力</button>
`
document.getElementById('start-boost').addEventListener('click', startBoost, { once: false })
}
function saveLog() {
const logArea = document.getElementById('log-area')
const logs = Array.from(logArea.children)
.map((el) => el.textContent)
.join('\n')
const now = new Date()
const filename = `115助力助手-${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(
2,
'0'
)}${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(
2,
'0'
)}.txt`
const blob = new Blob([logs], { type: 'text/plain;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
}
// 切换面板显示状态
function togglePanel() {
const panel = document.getElementById('boost-panel')
if (!panel) return
const currentRight = getComputedStyle(panel).right
if (currentRight === '0px') {
panel.style.right = '-320px'
} else {
panel.style.right = '0'
if (isMinimized) minimizePanel(false)
}
}
// 最小化/恢复面板
function minimizePanel(minimize = true) {
const panel = document.getElementById('boost-panel')
const content = document.getElementById('panel-content')
const minimizeBtn = document.getElementById('minimize-btn')
if (minimize) {
content.style.display = 'none'
panel.style.height = '52px'
minimizeBtn.textContent = '□'
isMinimized = true
} else {
content.style.display = 'block'
panel.style.height = '600px'
minimizeBtn.textContent = '−'
isMinimized = false
}
}
// 初始化函数
async function init() {
if (document.getElementById('boost-panel')) return
const toggleBtn = createToggleButton()
const panel = createPanel()
document.body.appendChild(toggleBtn)
document.body.appendChild(panel)
// 先获取用户信息
await fetchUserInfo()
// 绑定事件
document.getElementById('start-boost').addEventListener('click', startBoost, { once: false })
// 绑定复制按钮
document.getElementById('copy-user-id').addEventListener('click', function () {
const userId = document.getElementById('user-id').textContent
copyToClipboard(userId, this, '✅')
})
document.getElementById('copy-boost-code').addEventListener('click', function () {
const code = document.getElementById('my-boost-code').textContent
copyToClipboard(code, this, '✅')
})
// 最小化按钮
document.getElementById('minimize-btn').addEventListener('click', (e) => {
e.stopPropagation()
minimizePanel(!isMinimized)
})
// 关闭按钮
document.getElementById('close-btn').addEventListener('click', (e) => {
e.stopPropagation()
const panel = document.getElementById('boost-panel')
panel.style.right = '-320px'
})
// 面板头部拖动
const header = document.getElementById('panel-header')
let isDragging = false
let offsetX, offsetY
header.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'BUTTON') return
isDragging = true
offsetX = e.clientX - parseInt(panel.style.right || '0')
offsetY = e.clientY - parseInt(panel.style.top || '120px')
e.preventDefault()
})
document.addEventListener('mousemove', (e) => {
if (!isDragging) return
const right = window.innerWidth - (e.clientX + offsetX)
const top = e.clientY - offsetY
panel.style.top = `${Math.max(top, 0)}px`
panel.style.right = `${Math.max(right, 0)}px`
})
document.addEventListener('mouseup', () => {
isDragging = false
})
}
async function waitBaseInfo(timeout = 3000) {
const start = Date.now()
while (!baseInfoReady && Date.now() - start < timeout) await new Promise((r) => setTimeout(r, 50))
}
// ==================== init ====================
async function initSafe() {
await waitBaseInfo()
init()
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initSafe)
} else {
initSafe()
}
})()