您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
祝你好运
// ==UserScript== // @name V2ex Random Floor // @namespace http://tampermonkey.net/ // @version 2025-05-28 // @description 祝你好运 // @author You // @match https://www.v2ex.com/t/* // @icon https://www.google.com/s2/favicons?sz=64&domain=v2ex.com // @grant GM_registerMenuCommand // @require https://cdnjs.cloudflare.com/ajax/libs/seedrandom/3.0.5/seedrandom.js // @license MIT // ==/UserScript== (function () { 'use strict'; const V2exMaxPageSize = 20 // ==UserScript菜单== if (typeof GM_registerMenuCommand !== 'undefined') { GM_registerMenuCommand('启动抽奖', async function() { const urlTopicId = window.location.pathname.match(/\/t\/(\d+)/)[1]; const topicId = prompt('请输入主题ID:', urlTopicId || ''); if (!topicId) return alert('主题ID不能为空'); const defaultToken = window.localStorage.getItem('v2ex-random-floor-token') || ''; const userToken = prompt('请输入用户Token,在设置 - Tokens 中生成:', defaultToken); if (!userToken) return alert('用户Token不能为空'); window.localStorage.setItem('v2ex-random-floor-token', userToken); const luckyCount = parseInt(prompt('请输入抽奖人数:', '1'), 10) || 1; if (isNaN(luckyCount) || luckyCount < 1) return alert('抽奖人数必须大于0'); const isUnique = confirm('是否用户去重? (确定=是, 取消=否)'); const today = new Date(); const endTime = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1, 0, 0, 0); const deadline = prompt('请输入截止时间(可留空, 格式: yyyy-mm-dd HH:MM):', `${endTime.toISOString().slice(0, 10)} 24:00`); if (new Date(deadline).getTime() < 0) return alert('截止时间格式不正确'); const maxUserCount = parseInt(prompt('请输入最大参与人次(可留空):', '0'), 10) || 0; if (isNaN(maxUserCount) || maxUserCount < 0) return alert('最大参与人次必须大于等于0'); if (maxUserCount && maxUserCount < luckyCount) return alert('最大参与人次必须大于等于抽奖人数'); const randomSeed = prompt('请输入随机种子,如总楼层数,可引入外部随机变量保证公平性:', ''); const options = { topicId, luckyCount, isUnique, deadline, randomSeed, userToken, maxUserCount }; const rf = new RandomFloor(options); try { console.log(`开始抽奖: 主题ID=${rf.topicId}, 抽奖人数=${rf.luckyCount}, 用户去重=${rf.isUnique}, 截止时间=${rf.deadline}, 最大参与人次=${rf.maxUserCount}`); await rf.run(); alert('抽奖执行完毕, 结果请查看控制台日志'); } catch (e) { alert('抽奖出错: ' + e.message); } }); } class RandomFloor { constructor(options) { // 主题 ID this.topicId = options.topicId // 随机种子 this.nextRandomSeed = new Math.seedrandom(options.randomSeed || Math.random()) // 抽奖人数 this.luckyCount = options.luckyCount || 1 // 是否用户去重 this.isUnique = options.isUnique || false // 截止时间 this.deadline = options.deadline || 0 // 最多参与人次 this.maxUserCount = options.maxUserCount || 0 // 回帖列表 this.replies = [] // token this.token = options.userToken || '' } async run() { const replies = await this.getReplyList(this.topicId) const validateList = this.filterReplies(replies) this.addLog(`累计 ${replies.length} 条回帖,去重后 ${validateList.length} 条`) if (validateList.length < this.luckyCount) { this.addLog(`回帖数量不足,无法抽奖`) return } this.addLog(`开始抽奖...`) const nextId = this.nextRandomSeed const luckyList = [] const luckySet = new Set() while (luckyList.length < this.luckyCount) { const index = Math.floor(nextId() * validateList.length) const item = validateList[index] if (this.isUnique && luckySet.has(item.userId)) { continue } luckySet.add(item.userId) luckyList.push(item) } this.addLog(`抽奖完成,中奖名单如下:`) let messageText = `抽奖完成,中奖名单如下:\n` luckyList.forEach(item => { messageText += `第${item.index}楼 @${item.userName} (${item.userId})\n` }) this.addLog(messageText) this.addLog(`抽奖结束`) } get deadlineStamp() { if (typeof this.deadline === 'number') { return this.deadline } if (typeof this.deadline === 'string') { return new Date(this.deadline).getTime() } if (this.deadline instanceof Date) { return this.deadline.getTime() } return 0 } filterReplies(replies) { const { isUnique, deadlineStamp, maxUserCount } = this const userIds = new Set() const items = replies.filter(item => { if (isUnique && userIds.has(item.userId)) { return false } if (deadlineStamp && item.created > deadlineStamp) { return false } userIds.add(item.userId) return true }) return maxUserCount > 0 ? items.slice(0, maxUserCount) : items } async getReplyList(topicId, page = 1) { this.addLog(`获取楼层列表: 第${page}页 获取中...`) const replies = await fetch(`/api/v2/topics/${topicId}/replies?p=${page}`, { method: 'GET', headers: { Authorization: 'Bearer ' + this.token } }).then(res => { if (res.status !== 200) { throw new Error(`获取楼层列表失败: ${res.status} ${res.statusText}`) } return res.json() }) if (!replies.success) { throw new Error(`获取楼层列表失败: ${replies.message}`) } if (!replies.result.length) { return this.replies } const dataList = replies.result.map((item, index) => { return { index: this.replies.length + index + 1, userId: item.member.id, userName: item.member.username, created: item.created * 1000, } }) this.replies.push(...dataList) this.addLog(`获取楼层列表: 第${page}页 获取到 ${dataList.length} 条`) if (dataList.length < V2exMaxPageSize) { this.addLog(`获取楼层列表: 第${page}页 获取完毕`) return this.replies } return this.getReplyList(topicId, page + 1) } addLog(message) { console.info(`[v2ex RandomFloor] ${message}`) } } })();