- 楼主
刮刮乐目前每一批都要重复点买票、刮开,再继续买下一批,所以你就需要 烧饼刮刮乐助手,把这些重复操作自动完成。
支持选择面额和每批张数,统计投入、返还与净额,也可以设置中了百倍大奖或饼王徽奖池后停止。
当前版本:v1.1.0
运行方式:浏览器安装 Tampermonkey(油猴)后使用
适用页面:烧饼刮刮乐一、支持哪些功能?
功能 说明 自动买票、刮开 点击开始后,自动购买、刮开本批票,再继续下一批 面额选择 支持 10、25、50、100、200 游戏币 每批张数选择 支持一次购买 1、2、3、5、10 张同一面额的票 购买参数同步 助手面板与游戏页面的选项同步,并显示每批实际花费 大奖停止 百倍烤炉、饼王徽奖池可以分别勾选,命中后停止继续买票 批次控制 可设置批次间隔和批次上限,上限填 0 表示不限批次 本轮统计 显示购买批数、完成批数、完成张数、中奖张数、投入、返还和净额 旧票处理 页面恢复了未刮完的票时,先处理这些票 悬浮窗 支持拖动和收起,方便调整位置 脚本负责自动执行页面操作,开奖结果和奖金以站点结算为准。
二、怎么使用?
默认批次间隔为 3 秒,批次上限为 10 批。间隔支持 2~60 秒,批次上限填 0 表示持续运行,直到手动停止、命中已勾选的停止条件,或遇到异常。
批次上限按买票次数计算。 例如,每批 5 张、上限 10 批,正常完成时一共购买 50 张。
运行过程中会锁定面额和张数。需要修改时,先停止并处理完当前批次,再调整参数重新开始。
三、统计怎么看?
面板会显示:
- 新购批次、完成批次:分别表示本轮买下多少批、已经处理完成多少批。
- 完成张数、中奖张数:统计已完成批次中的票数和有奖票数。
- 投入、返还、净额:投入包含票价和抽成,返还按实际到账奖金累计,净额为返还减去投入。
例如,选择 200 面额、每批 2 张:
每批投入:412 游戏币 如果一张中百倍,另一张没中: 本批返还:20,000 游戏币 本批净额:+19,588 游戏币金额统计以本轮已完成的新购批次为准。 正在处理的批次,会在完成后计入;补刮之前留下的旧票不计入本轮统计。
“中奖张数” 包含 ×1 奖励,因此有奖不一定代表净赚。停止后再次点击 “开始”,会开启新一轮统计。
四、“中奖停止” 如何生效?
可以分别勾选:
- 中了百倍烤炉停止
- 中了饼王徽奖池停止
命中条件后,助手会完成当前批次的刮开与统计,然后停止购买下一批。
例如一次买了 10 张,其中一张中了目标奖项,其余票已经一起购买,仍会完成这一批的处理。希望按单张判断是否继续,可以把每批张数设为 1 张。
五、使用时留意这几点
- 选择面额和张数后,需要手动点击 “开始” 才会运行。
- 页面切到后台时,助手会暂缓新操作;回到前台后,继续仍在运行的任务。
- 余额不足、请求异常或页面状态无法识别时,助手会停止并显示原因。
- 网站改版后,脚本可能需要重新适配。
效果图
// ==UserScript== // @name 烧饼刮刮乐助手 // @namespace sb-scratch-helper // @version 1.1.0 // @description 面板选择面额和每批张数,自动买票刮开,支持限批、中奖停止及可拖动窗口。 // @match https://sb.sb/* // @run-at document-idle // @sandbox raw // @grant none // @noframes // ==/UserScript== /* * 安装:Tampermonkey 添加新脚本,替换全部内容并保存,然后刷新游戏页。 * 使用:在助手中选择面额与每批张数,再点击“开始”。默认最多新购10批,间隔3秒。 * 选项与原页面双向同步;运行中或当前票未处理完时锁定,停止且本批完成后可修改。 * 0批表示不限批次;首次可设为1批检查。百倍/奖池停止默认勾选,可自行取消。 * 统计:每次开始重置,只累计本轮新购且刮开确认完成的批次;补刮旧票不重复记账。 * 停止:不再发起新的点击;游戏已经发出的请求、已经开始的动画仍可能完成。 * 后台:暂停新操作,回前台继续;刷新、跳转或请求异常不会自动续跑。 * 拖动标题可移动窗口;收起按钮保留可拖动的标题栏。 */ (() => { 'use strict'; if (window.__SB_SCRATCH_HELPER_V1__) return; window.__SB_SCRATCH_HELPER_V1__ = true; const VERSION = '1.1.0'; const STORE = 'sb-scratch-helper-settings-v1'; const TIMEOUT = 120000; const DEFAULTS = { interval: 3, maxBatches: 10, stopTop: true, stopPool: true }; let ctx = null; let run = null; let host = null; let ui = null; let starting = false; let startEpoch = 0; let releaseLock = null; let serial = 0; let saved = {}; try { saved = JSON.parse(localStorage.getItem(STORE) || '{}'); } catch (_) {} if (!saved || typeof saved !== 'object') saved = {}; const settings = { ...DEFAULTS, ...saved }; const num = value => Number.isFinite(Number(value)) ? Number(value) : NaN; const tidy = value => String(value || '').replace(/\s+/g, ' ').trim(); const onPage = () => location.hostname === 'sb.sb' && /^\/games\/scratch\/?$/.test(location.pathname); const q = (selector, parent = ctx?.root) => parent?.querySelector(selector); const fmt = value => Number(value).toLocaleString('zh-CN'); const money = value => (value > 0 ? '+' : '') + fmt(value); function save() { try { localStorage.setItem(STORE, JSON.stringify(settings)); } catch (_) {} } function visible(el) { return !!el && !el.hidden && !!el.getClientRects().length && getComputedStyle(el).display !== 'none' && getComputedStyle(el).visibility !== 'hidden'; } function usable(el) { return visible(el) && !el.disabled && !el.classList.contains('is-busy') && el.getAttribute('aria-busy') !== 'true' && el.getAttribute('aria-disabled') !== 'true'; } function state() { const button = ctx?.main; if (!button) return 'unknown'; const label = tidy(button.getAttribute('aria-label')); if (button.getAttribute('aria-busy') === 'true' || /出票中/.test(label)) return 'waiting'; if (button.classList.contains('is-busy')) return 'wiping'; if (/^刮开(?:这张)?$/.test(label)) return 'ready'; if (label === '下一张') return 'shown'; if (!ctx.form.classList.contains('is-batch') && /买\s*\d+\s*张/.test(label)) { return label.startsWith('再来一批') ? 'done' : 'idle'; } return 'unknown'; } function selected() { const bet = num(q('input[name="bet"]:checked', ctx.form)?.value); const count = num(q('input[name="n"]:checked', ctx.form)?.value); if (!Number.isInteger(bet) || bet <= 0 || ![1, 2, 3, 5, 10].includes(count)) { throw new Error('没有读到有效的面额和数量,请先在游戏中选择。'); } return { bet, count }; } function nativeChoices(name) { if (!ctx?.form || !['bet', 'n'].includes(name)) return []; return Array.from(ctx.form.querySelectorAll(`input[name="${name}"]`)) .filter(input => Number.isInteger(Number(input.value)) && Number(input.value) > 0); } function canEditPurchaseChoices() { const record = ctx?.current; const batchReady = !record || record.finished || (record.status !== 'pending' && record.displayDone && allAcknowledged(record) && !record.pollsPending); return !!ctx?.live && !run?.running && !starting && ctx.buyPending === 0 && batchReady && ['idle', 'done'].includes(state()); } function syncPurchaseChoices() { if (!ui?.bet || !ui?.count || !ctx) return; const editable = canEditPurchaseChoices(); for (const [name, field, unit, runKey] of [ ['bet', ui.bet, ' 游戏币', 'bet'], ['n', ui.count, ' 张', 'count'] ]) { const inputs = nativeChoices(name); const signature = inputs.map(input => `${input.value}:${!!input.disabled}`).join('|'); if (field.dataset.choices !== signature) { field.replaceChildren(); for (const input of inputs) { const option = document.createElement('option'); option.value = String(input.value); option.textContent = input.value + unit + (input.disabled ? '(不可选)' : ''); option.disabled = !!input.disabled; field.appendChild(option); } field.dataset.choices = signature; } const current = inputs.find(input => input.checked); field.value = String(run?.running ? run[runKey] : current?.value || ''); field.disabled = !editable || inputs.length === 0; } if (ui.purchaseHint) { ui.purchaseHint.textContent = run?.running || starting ? '本轮面额和张数已锁定,停止并完成当前批次后可修改。' : editable ? '与游戏页面同步;仅更改选项,点击“开始”后才会自动买票。' : '当前还有票正在处理,完成这一批后可修改面额和张数。'; } } function changePurchaseChoice(name, value) { if (!canEditPurchaseChoices()) { draw(); status('请先停止助手并完成当前批次,再修改面额和张数。'); return false; } const input = nativeChoices(name).find(item => Number(item.value) === Number(value)); if (!input || input.disabled) { draw(); status('该选项当前不可用,请检查游戏币余额或选择其他档位。', true); return false; } const before = selected(); ctx.syncingChoices = true; try { // 原页面 change 会执行金额重算和 afford;这里只改选项,不提交表单。 input.checked = true; input.dispatchEvent(new Event('change', { bubbles: true })); const actual = selected(); draw(); const key = name === 'bet' ? 'bet' : 'count'; const other = name === 'bet' ? 'count' : 'bet'; if (actual[key] !== Number(value)) { status('原页面没有接受该选项,已显示实际可用的面额和张数。', true); return false; } const description = `${actual.bet} 币 × ${actual.count} 张,每批 ${fmt(cost(actual.bet, actual.count))} 游戏币。`; if (actual[other] !== before[other]) { status('原页面同时调整了另一项,请确认:' + description); } else { status('已选择:' + description + '点击“开始”运行。'); } return true; } catch (error) { draw(); status('选项同步失败:' + (error.message || '请检查页面'), true); return false; } finally { ctx.syncingChoices = false; } } function fee(bet) { return Math.floor(bet * ctx.rake / 100); } function cost(bet, count) { return (bet + fee(bet)) * count; } function balance() { for (const el of document.querySelectorAll('[data-wheel-coins]')) { const text = tidy(el.textContent).replace(/,/g, ''); if (/^\d+$/.test(text)) return Number(text); } return ctx.coins; } function netOf(record) { return record.tickets.reduce( (sum, ticket) => sum + ticket.paid - ticket.bet - fee(ticket.bet), 0 ); } function endpoint(value) { if (!value) return ''; const url = new URL(value, location.href); if (url.origin !== location.origin) { throw new Error('游戏接口地址发生变化,请停止并检查页面。'); } url.hash = ''; return url.href; } function status(text, error = false) { if (!ui) return; ui.status.textContent = text; ui.status.classList.toggle('error', error); } function log(text) { const line = new Date().toLocaleTimeString('zh-CN', { hour12: false }) + ' ' + text; if (ui) { ui.lines.unshift(line); ui.lines = ui.lines.slice(0, 7); ui.log.textContent = ui.lines.join('\n'); } console.info('[刮刮乐助手]', text); } function draw() { if (!ui) return; const active = !!run?.running; ui.start.disabled = active || starting; ui.stop.disabled = !active && !starting; ui.fields.forEach(el => { el.disabled = active || starting; }); syncPurchaseChoices(); const stats = run?.stats || { bought: 0, completed: 0, tickets: 0, wins: 0, spent: 0, paid: 0 }; ui.stats.textContent = `新购 ${stats.bought} 批 · 完成 ${stats.completed} 批\n` + `完成 ${stats.tickets} 张 · 中奖 ${stats.wins} 张\n` + `投入 ${fmt(stats.spent)} · 返还 ${fmt(stats.paid)}\n` + `净额 ${money(stats.paid - stats.spent)} 游戏币`; try { const plan = active ? run : selected(); ui.plan.textContent = `${plan.bet} 币 × ${plan.count} 张 · 每批 ${fmt(cost(plan.bet, plan.count))} 币(含抽成)`; } catch (_) { ui.plan.textContent = '等待游戏加载或选择面额'; } } function stop(reason, error = false) { startEpoch++; const active = run?.running; if (run) run.running = false; starting = false; if (releaseLock) { const release = releaseLock; releaseLock = null; release(); } status(reason, error); if (active || error) log(reason); draw(); } function fail(message, uncertain = false, target = ctx) { if (!target || target !== ctx || !target.live) return; if (uncertain) target.uncertain = true; target.lastError = message; if (run?.running) stop(message, true); else status(message, true); } function touch() { if (run?.running) run.waitSince = Date.now(); } // 只观察原页面的 fetch:原参数、原 Promise 和原响应均原样交还页面。 // 不创建购买、开奖轮询或揭示请求,不修改 request_id,不读取或导出 Cookie。 function observeFetch(target) { const original = window.fetch; function watched(input, init) { let route = null; let method = 'GET'; try { const rawURL = typeof input === 'string' || input instanceof URL ? String(input) : input.url; method = String(init?.method || input?.method || 'GET').toUpperCase(); route = target.live ? target.routes.get(endpoint(rawURL)) : null; if (route && method !== (route.kind === 'poll' ? 'GET' : 'POST')) { route = null; } } catch (_) { route = null; } const owner = route?.kind === 'buy' ? target.expectedBuy : null; if (route?.kind === 'buy') { target.expectedBuy = null; target.buyPending++; if (run?.running && !owner) { stop('检测到手动买票,助手已停止,请等这批完成。'); } } if (route?.kind === 'reveal') { route.record.acks.set(route.url, 'pending'); route.record.revealStarted = true; } if (route?.kind === 'poll') { route.record.pollsPending++; if ( route.record.revealClicked || route.record.revealStarted || route.record.finished ) { fail( '页面在刮开后再次查询旧批次,已停止;请刷新以同步票据状态。', true, target ); } } let promise; try { promise = Reflect.apply(original, this, arguments); } catch (error) { if (route?.kind === 'buy') target.buyPending--; if (route?.kind === 'poll') route.record.pollsPending--; if (route) { fail( '游戏请求没有发出:' + (error.message || '未知错误'), route.kind !== 'poll', target ); } throw error; } if (route) { Promise.resolve(promise).then(async response => { if (!target.live || target !== ctx) return; if (route.kind === 'poll' && route.record !== target.current) { fail( '收到了上一批迟到的开奖响应,已停止;请刷新以同步票据状态。', true, target ); return; } if (!response.ok) { throw new Error(`HTTP ${response.status}`); } if ( response.redirected && response.url && /\/login(?:\/|\?)/.test(response.url) ) { throw new Error('登录已失效'); } const body = await response.clone().text(); if (!target.live || target !== ctx) return; if (route.kind === 'reveal') { if (/^\s*</.test(body)) { throw new Error('服务器返回了网页,请检查登录状态'); } if (body.trim().startsWith('{')) { const data = JSON.parse(body); if (data.error || data.ok === false || data.success === false) { throw new Error( String(data.error || data.message || '揭示未成功') ); } } route.record.acks.set(route.url, 'ok'); touch(); return; } const data = JSON.parse(body); if (data?.error) { if ( route.kind === 'buy' && ['pending', 'unscratched'].includes(data.code) && data.batch ) { const recovered = newRecord(data.batch, null, target); if (run?.running) { run.record = recovered; run.awaitingBuy = false; log('站点返回尚未完成的旧票,先补刮;不计为本轮新购。'); } return; } fail('游戏提示:' + String(data.error), false, target); return; } if (route.kind === 'buy') { const record = newRecord(data, owner, target); if (owner) owner.stats.bought++; if (run?.running && owner === run) { run.record = record; run.awaitingBuy = false; log(`买票已受理:${run.bet} 币 × ${run.count} 张,等待开奖。`); } draw(); } else { updateRecord(route.record, data, target); } }).catch(error => { if (route.kind === 'reveal') { route.record.acks.set(route.url, 'failed'); } const name = route.kind === 'buy' ? '买票' : route.kind === 'poll' ? '开奖查询' : '刮开确认'; fail( `${name}异常:${error.message || '网络错误'}。助手已停,请核对页面后刷新。`, route.kind !== 'poll', target ); }).finally(() => { if (route.kind === 'buy') target.buyPending--; if (route.kind === 'poll') route.record.pollsPending--; }).catch(() => {}); } return promise; } window.fetch = watched; target.restoreFetch = () => { if (window.fetch === watched) window.fetch = original; }; } function updateRecord(record, data, target) { if (!data || typeof data !== 'object' || !Array.isArray(data.tickets)) { throw new Error('没有识别到批次数据,请刷新页面后重试。'); } record.status = data.status; record.count = num(data.n); if (Number.isFinite(num(data.coins))) { target.coins = num(data.coins); } const jsonURL = endpoint(data.json_url); if (jsonURL) { target.routes.set(jsonURL, { kind: 'poll', record, url: jsonURL }); } record.tickets = data.tickets.map(ticket => ({ id: String(ticket.id ?? ''), bet: num(ticket.bet), paid: num(ticket.paid), symbol: num(ticket.symbol), revealed: !!ticket.revealed, url: endpoint(ticket.reveal_url) })); for (const ticket of record.tickets) { if (ticket.url) { target.routes.set(ticket.url, { kind: 'reveal', record, url: ticket.url }); if (ticket.revealed) { record.acks.set(ticket.url, 'ok'); } else if (!record.acks.has(ticket.url)) { record.acks.set(ticket.url, 'needed'); } } } if (record.status !== 'pending') { const ids = new Set(record.tickets.map(ticket => ticket.id)); if ( !record.tickets.length || record.count !== record.tickets.length || ids.size !== record.count || record.tickets.some(ticket => !ticket.id || !Number.isInteger(ticket.bet) || ticket.bet <= 0 || !Number.isFinite(ticket.paid) || ticket.paid < 0 || !Number.isInteger(ticket.symbol) || ticket.symbol < -1 || ticket.symbol > 8 || (!ticket.revealed && !ticket.url) ) ) { throw new Error('开奖字段与当前脚本不兼容,已停止自动操作。'); } if ( record.owner && ( record.count !== record.owner.count || record.tickets.some(ticket => ticket.bet !== record.owner.bet) ) ) { throw new Error('返回的面额或张数与启动设置不同,请检查本批票。'); } record.serverDone = record.tickets.every(ticket => ticket.revealed); if (record.serverDone) { record.displayDone = true; } if (record.owner && !record.settledLogged) { record.settledLogged = true; log( `开奖完成:${record.count} 张,返还 ` + `${fmt(record.tickets.reduce((sum, ticket) => sum + ticket.paid, 0))} 游戏币。` ); } } } function newRecord(data, owner, target) { // 一个时刻只跟踪当前批次,避免无限运行时积累旧票和旧 URL。 target.routes.clear(); target.routes.set(target.buyURL, { kind: 'buy', url: target.buyURL }); const record = { serial: ++serial, owner, tickets: [], acks: new Map(), pollsPending: 0, revealStarted: false, revealClicked: false, displayDone: false, serverDone: false, finished: false }; updateRecord(record, data, target); target.current = record; touch(); return record; } function allAcknowledged(record) { return record.tickets.length > 0 && record.tickets.every(ticket => ticket.revealed || (ticket.url && record.acks.get(ticket.url) === 'ok') ); } function complete(record) { record.finished = true; if (record.owner === run) { const stats = run.stats; stats.completed++; stats.tickets += record.tickets.length; stats.wins += record.tickets.filter(ticket => ticket.symbol >= 0).length; stats.spent += record.tickets.reduce( (sum, ticket) => sum + ticket.bet + fee(ticket.bet), 0 ); stats.paid += record.tickets.reduce( (sum, ticket) => sum + ticket.paid, 0 ); log(`第 ${stats.completed} 批完成,净额 ${money(netOf(record))} 游戏币。`); } else { log('旧票已补刮完成,不计入本轮投入和返还。'); } run.record = null; run.waitSince = Date.now(); run.nextBuyAt = Date.now() + run.interval * 1000; draw(); if (run.stopPool && record.tickets.some(ticket => ticket.symbol === 8)) { stop('中了饼王徽奖池,已按设置停止。'); } else if (run.stopTop && record.tickets.some(ticket => ticket.symbol === 7)) { stop('中了百倍烤炉,已按设置停止。'); } else if ( run.maxBatches > 0 && run.stats.bought >= run.maxBatches ) { stop(`已完成本轮 ${run.stats.completed} 批,达到批次上限。`); } } function begin() { if (!ctx?.live || !onPage()) { throw new Error('请在刮刮乐页面启动。'); } if (ctx.uncertain) { throw new Error( '上一请求结果不确定,请刷新游戏页,让原页面恢复票据后再开始。' ); } const plan = selected(); const interval = Number(ui.interval.value); const maxBatches = Number(ui.maxBatches.value); if (!Number.isFinite(interval) || interval < 2 || interval > 60) { throw new Error('批次间隔请填写 2~60 秒。'); } if ( !Number.isInteger(maxBatches) || maxBatches < 0 || maxBatches > 100000 ) { throw new Error( '批次上限请填写 0~100000 的整数;0 表示不限批次。' ); } Object.assign(settings, { interval, maxBatches, stopTop: ui.stopTop.checked, stopPool: ui.stopPool.checked }); save(); run = { ...settings, ...plan, running: true, context: ctx, awaitingBuy: false, waitSince: Date.now(), hiddenAt: 0, nextBuyAt: Date.now(), record: null, stats: { bought: 0, completed: 0, tickets: 0, wins: 0, spent: 0, paid: 0 } }; if ( ctx.current && !ctx.current.finished && ( !ctx.current.serverDone || !['idle', 'done'].includes(state()) ) ) { run.record = ctx.current; log('先处理页面已有的票,再开始本轮新购。'); } starting = false; ctx.lastError = ''; log( `开始:固定 ${plan.bet} 币 × ${plan.count} 张,` + `${maxBatches ? '最多新购 ' + maxBatches + ' 批' : '不限批次'}。` ); status('已开始,正在检查页面状态。'); draw(); } async function start() { if (starting || run?.running) return; const epoch = ++startEpoch; const startContext = ctx; starting = true; draw(); try { if (navigator.locks?.request) { await navigator.locks.request( 'sb-scratch-helper-run-v1', { ifAvailable: true }, lock => { if ( epoch !== startEpoch || ctx !== startContext || !starting ) { return; } if (!lock) { throw new Error( '另一个标签页的刮刮乐助手正在运行,请先停止那一页。' ); } return new Promise(resolve => { releaseLock = resolve; try { begin(); } catch (error) { stop(error.message, true); } }); } ); } else if (epoch === startEpoch && ctx === startContext) { begin(); } } catch (error) { if (epoch === startEpoch) { stop(error.message || '无法启动助手。', true); } } finally { if (epoch === startEpoch) { starting = false; draw(); } } } function tick() { if (!run?.running || !ctx) return; if ( run.context !== ctx || !onPage() || !ctx.root.isConnected ) { stop('游戏页面已切换,助手已停止。'); return; } const now = Date.now(); if (document.hidden) { if (!run.hiddenAt) run.hiddenAt = now; status('页面在后台,暂停新操作;返回页面后继续。'); return; } if (run.hiddenAt) { const elapsed = now - run.hiddenAt; // 后台可能收到响应并更新 waitSince; // 回前台重新给足等待时间,避免双重累加。 run.waitSince = now; run.nextBuyAt += elapsed; run.hiddenAt = 0; } if (now - run.waitSince > TIMEOUT) { stop( '等待超过 120 秒,助手已停止。请检查原页面是否仍在开奖或有错误。', true ); return; } const currentState = state(); const pageMessage = tidy(q('[data-sc-msg]')?.textContent); if ( /游戏币不够|网络不太稳|暂停卖票|登录.*(?:失效|过期)|请.*登录|验证码|请求.*频繁|限流/.test( pageMessage ) ) { stop(pageMessage, true); return; } if (run.awaitingBuy) { status('买票请求处理中,等待原页面开奖。'); return; } if (!run.record && !['idle', 'done'].includes(currentState)) { if (ctx.current && !ctx.current.finished) { run.record = ctx.current; } else { status('等待游戏返回当前批次数据;长时间无变化请刷新。'); return; } } const record = run.record; if (record) { if (record.pollsPending > 0) { status('等待原页面的开奖查询全部返回。'); return; } if (record.status === 'pending' || currentState === 'waiting') { status( tidy(q('[data-sc-wait-text]')?.textContent) || '等待开奖,尚未购买下一批。' ); return; } if ( allAcknowledged(record) && record.displayDone && ['done', 'idle'].includes(currentState) ) { complete(record); return; } const revealPending = Array.from(record.acks.values()).includes('pending'); if ( record.revealClicked || revealPending || ['done', 'wiping'].includes(currentState) ) { const confirmed = record.tickets.filter(ticket => ticket.revealed || record.acks.get(ticket.url) === 'ok' ).length; status( `等待刮开及服务器确认:${confirmed}/${record.tickets.length} 张。` ); return; } if (['ready', 'shown'].includes(currentState)) { let button = null; if (usable(ctx.all)) { button = ctx.all; } else if (currentState === 'ready' && usable(ctx.main)) { button = ctx.main; } if (!button) { status('等待刮开按钮就绪。'); return; } record.revealClicked = true; touch(); status( button === ctx.all ? '正在全部刮开。' : '正在刮开当前这张。' ); button.click(); return; } status('等待本批显示完成。'); return; } if (!['idle', 'done'].includes(currentState)) return; if ( run.maxBatches > 0 && run.stats.bought >= run.maxBatches ) { stop('已达到本轮购买批次上限。'); return; } const plan = selected(); if (plan.bet !== run.bet || plan.count !== run.count) { stop( `页面选项已变为 ${plan.bet} 币 × ${plan.count} 张,` + '助手已停止。请确认余额和选项后重新开始。', true ); return; } const coins = balance(); const required = cost(run.bet, run.count); if (!Number.isFinite(coins) || coins < required) { stop( Number.isFinite(coins) ? `余额 ${fmt(coins)},下一批需要 ${fmt(required)} 游戏币,已停止。` : '无法确认当前游戏币余额,已停止。', true ); return; } const labelCost = tidy( ctx.main.getAttribute('aria-label') ).match(/共\s*(\d+)\s*游戏币/); if (!labelCost || Number(labelCost[1]) !== required) { stop( '页面显示的购买金额与计算结果不同,已停止,请检查抽成规则。', true ); return; } if (now < run.nextBuyAt) { status( `本批已确认,${Math.ceil((run.nextBuyAt - now) / 1000)} 秒后买下一批。` ); return; } if (ctx.main.disabled) { stop('当前游戏暂停卖票,助手已停止。', true); return; } if (!usable(ctx.main) || ctx.buyPending > 0) { status('等待购买按钮就绪。'); return; } run.awaitingBuy = true; ctx.expectedBuy = run; touch(); status(`正在购买第 ${run.stats.bought + 1} 批。`); ctx.main.click(); // 原页面买票应同步发出 fetch; // 不成功时停下,绝不重复连点。 if (ctx.expectedBuy === run) { ctx.expectedBuy = null; stop( tidy(q('[data-sc-msg]')?.textContent) || '原页面没有发出买票请求,已停止。', true ); } } function panel() { host = document.createElement('div'); host.id = 'sb-scratch-helper'; host.style.cssText = 'position:fixed;right:18px;top:90px;' + 'z-index:2147483646;width:310px;' + 'max-width:calc(100vw - 16px);'; const shadow = host.attachShadow({ mode: 'open' }); shadow.innerHTML = ` <style> :host { font:13px/1.55 system-ui,-apple-system,"Microsoft YaHei",sans-serif; color:#eff7f1; color-scheme:dark; } * { box-sizing:border-box; } .box { border:1px solid #426d58; border-radius:13px; background:#14251f; box-shadow:0 8px 32px #0007; overflow:hidden; } header { display:flex; align-items:center; gap:8px; padding:11px 13px; background:#203b2e; cursor:move; touch-action:none; user-select:none; } header b { font-size:14px; flex:1; } header small { color:#b5c9bd; } button,input,select { font:inherit; } button { cursor:pointer; border:0; border-radius:7px; padding:8px 12px; } button:disabled { cursor:default; opacity:.45; } .fold { padding:0 5px; background:transparent; color:#e1eddf; font-size:20px; } .body { padding:12px; max-height:calc(100vh - 140px); overflow:auto; } .plan { color:#eed890; margin-bottom:10px; font-size:12px; } .row { display:flex; align-items:center; justify-content:space-between; margin:7px 0; } input[type=number],select { width:118px; padding:5px 7px; background:#0d1d17; border:1px solid #426d58; border-radius:6px; color:#fff; } select:disabled { opacity:.65; cursor:not-allowed; } input[type=checkbox] { accent-color:#edc369; } .hint { font-size:11px; color:#a9bcb0; } .checks { display:flex; gap:12px; flex-wrap:wrap; margin:10px 0; } .buttons { display:flex; gap:8px; } .start { flex:1; background:#e9bd65; color:#201709; font-weight:700; } .stop { flex:1; background:#713f3a; color:#fff; } .status { padding:9px; margin-top:10px; border-radius:7px; background:#0c1b15; overflow-wrap:anywhere; } .status.error { color:#ffbdac; } .stats { white-space:pre-line; margin:8px 0; color:#d6e8d9; font-variant-numeric:tabular-nums; } details { border-top:1px solid #345342; margin-top:8px; padding-top:7px; } summary { cursor:pointer; color:#b8cbbf; } pre { white-space:pre-wrap; overflow-wrap:anywhere; font-family:inherit; font-size:11px; line-height:1.6; color:#b8cbbf; max-height:145px; overflow:auto; } [hidden] { display:none!important; } </style> <section class="box"> <header> <b>刮刮乐助手</b> <small>v${VERSION}</small> <button class="fold" title="收起/展开">−</button> </header> <div class="body"> <div class="plan" data-role="plan"></div> <label class="row"> 面额 <select data-field="bet" aria-label="面额(游戏币)" ></select> </label> <label class="row"> 每批张数 <select data-field="count" aria-label="每批张数" ></select> </label> <div class="hint" data-role="purchaseHint"></div> <label class="row"> 每批间隔(秒) <input data-field="interval" type="number" min="2" max="60" step="1" > </label> <label class="row"> 新购批次上限 <input data-field="maxBatches" type="number" min="0" max="100000" step="1" > </label> <div class="hint"> 上限填 0 表示不限批次;总张数 = 每批张数 × 批次数。 </div> <div class="checks"> <label> <input data-field="stopTop" type="checkbox"> 中百倍即停 </label> <label> <input data-field="stopPool" type="checkbox"> 中奖池即停 </label> </div> <div class="buttons"> <button class="start" data-action="start"> 开始 </button> <button class="stop" data-action="stop" disabled> 停止 </button> </div> <div class="status" data-role="status"> 已加载,选择面额和数量后点击开始。 </div> <div class="hint" style="margin-top:9px"> 本轮已完成批次的统计(含抽成) </div> <div class="stats" data-role="stats"></div> <div class="hint"> 拖动标题移动窗口。后台暂停;刷新不自动续跑。 </div> <details> <summary>运行记录</summary> <pre data-role="log"></pre> </details> </div> </section> `; const pick = selector => shadow.querySelector(selector); ui = { start: pick('[data-action=start]'), stop: pick('[data-action=stop]'), bet: pick('[data-field=bet]'), count: pick('[data-field=count]'), purchaseHint: pick('[data-role=purchaseHint]'), interval: pick('[data-field=interval]'), maxBatches: pick('[data-field=maxBatches]'), stopTop: pick('[data-field=stopTop]'), stopPool: pick('[data-field=stopPool]'), plan: pick('[data-role=plan]'), status: pick('[data-role=status]'), stats: pick('[data-role=stats]'), log: pick('[data-role=log]'), lines: [], fields: Array.from(shadow.querySelectorAll('input,select')) }; ui.interval.value = Number.isFinite(num(settings.interval)) ? settings.interval : 3; ui.maxBatches.value = Number.isInteger(num(settings.maxBatches)) ? settings.maxBatches : 10; ui.stopTop.checked = !!settings.stopTop; ui.stopPool.checked = !!settings.stopPool; ui.bet.addEventListener('change', () => { changePurchaseChoice('bet', ui.bet.value); }); ui.count.addEventListener('change', () => { changePurchaseChoice('n', ui.count.value); }); ui.start.addEventListener('click', start); ui.stop.addEventListener('click', () => { stop( '已手动停止。已经发出的请求仍由游戏处理,助手不会继续买票。' ); }); pick('.fold').addEventListener('click', () => { const body = pick('.body'); body.hidden = !body.hidden; pick('.fold').textContent = body.hidden ? '+' : '−'; }); let drag = null; const header = pick('header'); header.addEventListener('pointerdown', event => { if (event.button !== 0 || event.target.closest('button')) return; const rect = host.getBoundingClientRect(); drag = { x: event.clientX - rect.left, y: event.clientY - rect.top }; header.setPointerCapture(event.pointerId); event.preventDefault(); }); header.addEventListener('pointermove', event => { if (!drag) return; host.style.right = 'auto'; host.style.left = Math.max( 4, Math.min( innerWidth - host.offsetWidth - 4, event.clientX - drag.x ) ) + 'px'; host.style.top = Math.max( 4, Math.min( innerHeight - 48, event.clientY - drag.y ) ) + 'px'; }); function endDrag() { if (!drag) return; drag = null; const rect = host.getBoundingClientRect(); settings.position = { left: rect.left, top: rect.top }; save(); } header.addEventListener('pointerup', endDrag); header.addEventListener('pointercancel', endDrag); (document.fullscreenElement || document.body).appendChild(host); if ( settings.position && Number.isFinite(settings.position.left) && Number.isFinite(settings.position.top) ) { host.style.right = 'auto'; host.style.left = Math.max( 4, Math.min( innerWidth - host.offsetWidth - 4, settings.position.left ) ) + 'px'; host.style.top = Math.max( 4, Math.min( innerHeight - 48, settings.position.top ) ) + 'px'; } draw(); } function detach() { if (run?.running || starting) { stop('页面已切换,助手已停止。'); } if (ctx) { ctx.live = false; ctx.restoreFetch?.(); if (ctx.onChoiceChange) { ctx.form.removeEventListener('change', ctx.onChoiceChange); } } host?.remove(); host = ui = ctx = null; } function discover() { const root = onPage() ? document.querySelector('[data-sc]') : null; if (ctx?.root === root && root?.isConnected) return; if (ctx) detach(); if (!root) return; try { const configEl = document.getElementById('scratch-config'); const config = configEl ? JSON.parse(configEl.textContent) : null; const form = root.querySelector('[data-sc-form]'); const main = root.querySelector('[data-sc-buy]'); if ( !config?.init || !form || !main || !config.buy || !main.getAttribute('aria-label') ) { return; } const rake = parseInt(form.getAttribute('data-rake-pct'), 10) || 0; if (rake < 0 || rake > 100) return; ctx = { root, form, main, all: root.querySelector('[data-sc-all]'), live: true, routes: new Map(), buyURL: endpoint(config.buy), coins: num(config.init.coins), rake, current: null, expectedBuy: null, buyPending: 0, uncertain: false, lastError: '' }; ctx.routes.set(ctx.buyURL, { kind: 'buy', url: ctx.buyURL }); if (config.init.open) { newRecord(config.init.open, null, ctx); } observeFetch(ctx); panel(); const target = ctx; target.onChoiceChange = event => { if ( !['bet', 'n'].includes(event.target?.name) || target.syncingChoices ) { return; } queueMicrotask(() => { if (ctx === target && target.live) draw(); }); }; form.addEventListener('change', target.onChoiceChange); log('助手加载完成,等待手动开始。'); } catch (error) { console.warn( '[刮刮乐助手] 页面暂未就绪:', error.message ); if (ctx) detach(); } } window.addEventListener('bbs:game', event => { if (!ctx?.current || !onPage()) return; const record = ctx.current; if ( record.status !== 'pending' && Number.isFinite(event.detail?.net) && netOf(record) === event.detail.net ) { record.displayDone = true; touch(); } }); window.addEventListener('bbs:toast', event => { if ( run?.running && onPage() && event.detail?.error ) { stop( String(event.detail.message || '页面提示错误。'), true ); } }); document.addEventListener('fullscreenchange', () => { if (host) { (document.fullscreenElement || document.body).appendChild(host); } }); window.addEventListener('pagehide', () => { if (run?.running) { stop('页面已离开,助手已停止。'); } }); let lastDiscover = 0; function loop() { try { if (Date.now() - lastDiscover > 900) { lastDiscover = Date.now(); discover(); if (!run?.running) draw(); } tick(); } catch (error) { stop( '助手已停止:' + (error.message || '未知错误'), true ); } setTimeout(loop, 250); } loop(); })();收藏支持 1反对打赏 1个性签名学习的能力和方法比知识本身更重要
好的,等下用用
大佬强啊,这是百倍饼王吧
可以的,解放双手
人才辈出
自动开刷
烧饼燃烧器
自动刮票省了不少事
个性签名我是joker,你好,我要梭哈了
