// ==UserScript==
// @name B站抽奖动态智能处理
// @namespace https://github.com/bilibili-lottery-handler
// @version 1.0.2
// @description 智能处理B站抽奖动态:未开奖保留,已开奖判断UP主其他抽奖后决定是否取关
// @author senjoke
// @match http*://*.bilibili.com/*
// @icon https://www.bilibili.com/favicon.ico
// @grant GM_info
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_notification
// @grant GM_registerMenuCommand
// @run-at document-end
// @require https://unpkg.com/axios/dist/axios.min.js
// ==/UserScript==
(function() {
'use strict';
/**
* 初始化配置
*/
function initConfig() {
if (GM_getValue('smart-unfollow') == undefined) {
GM_setValue('smart-unfollow', true);
}
if (GM_getValue('delete-finished-lottery') == undefined) {
GM_setValue('delete-finished-lottery', true);
}
if (GM_getValue('unfollow-list') == undefined) {
GM_setValue('unfollow-list', []);
}
// 新增配置:删除重试次数
if (GM_getValue('delete-retry-count') == undefined) {
GM_setValue('delete-retry-count', 3);
}
// 新增配置:操作延迟时间(毫秒)
if (GM_getValue('operation-delay') == undefined) {
GM_setValue('operation-delay', 1500);
}
}
/**
* 全局状态管理
*/
let isProcessing = false;
let currentProcessId = null;
/**
* 弹窗样式
*/
const styles = `
.lottery-handler-popup {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 10000;
display: flex;
justify-content: center;
align-items: center;
}
.lottery-handler-content {
background-color: #fff;
border-radius: 10px;
width: 500px;
max-height: 80vh;
padding: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
position: relative;
}
.lottery-handler-content.large {
width: 700px;
max-height: 90vh;
}
.lottery-handler-header {
font-size: 20px;
font-weight: bold;
margin-bottom: 20px;
text-align: center;
color: #333;
display: flex;
justify-content: space-between;
align-items: center;
}
.lottery-handler-body {
max-height: 60vh;
overflow-y: auto;
margin-bottom: 20px;
}
.config-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.config-item:last-child {
border-bottom: none;
}
.config-label {
font-size: 14px;
color: #333;
}
.config-input {
margin-left: 10px;
}
.lottery-handler-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background-color: #00a1d6;
color: white;
}
.btn-primary:hover:not(:disabled) {
background-color: #0082b3;
}
.btn-secondary {
background-color: #ccc;
color: #333;
}
.btn-secondary:hover:not(:disabled) {
background-color: #bbb;
}
.btn-success {
background-color: #52c41a;
color: white;
}
.btn-success:hover:not(:disabled) {
background-color: #389e0d;
}
.progress-bar {
width: 100%;
height: 20px;
background-color: #f0f0f0;
border-radius: 10px;
overflow: hidden;
margin: 10px 0;
}
.progress-fill {
height: 100%;
background-color: #00a1d6;
transition: width 0.3s ease;
}
.status-text {
font-size: 12px;
color: #666;
text-align: center;
margin-top: 5px;
}
.minimize-btn {
background: #f0f0f0;
border: none;
border-radius: 3px;
width: 30px;
height: 30px;
cursor: pointer;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.minimize-btn:hover {
background: #e0e0e0;
}
.mini-progress {
position: fixed;
top: 20px;
right: 20px;
width: 300px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
padding: 15px;
z-index: 10001;
display: none;
}
.mini-progress.show {
display: block;
}
.mini-header {
font-size: 14px;
font-weight: bold;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.expand-btn {
background: #00a1d6;
color: white;
border: none;
border-radius: 3px;
width: 24px;
height: 24px;
cursor: pointer;
font-size: 12px;
}
.result-section {
margin: 15px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #00a1d6;
}
.result-title {
font-weight: bold;
margin-bottom: 10px;
color: #333;
}
.result-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
margin-bottom: 15px;
}
.stat-item {
text-align: center;
padding: 10px;
background: white;
border-radius: 6px;
border: 1px solid #e0e0e0;
}
.stat-number {
font-size: 18px;
font-weight: bold;
color: #00a1d6;
}
.stat-label {
font-size: 12px;
color: #666;
margin-top: 5px;
}
.user-list {
max-height: 200px;
overflow-y: auto;
background: white;
border-radius: 6px;
padding: 10px;
}
.user-item {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.user-item:last-child {
border-bottom: none;
}
.user-name {
font-weight: 500;
color: #333;
}
.user-uid {
font-size: 12px;
color: #999;
margin-left: 8px;
}
.no-data {
text-align: center;
color: #999;
font-style: italic;
padding: 20px;
}
`;
GM_addStyle(styles);
/**
* 获取Cookie值
* @param {string} key Cookie键名
* @returns {string} Cookie值
*/
function getCookie(key) {
const cookieArr = document.cookie.split(';');
for (let i = 0; i < cookieArr.length; i++) {
const cookie = cookieArr[i].trim();
if (cookie.indexOf(key + '=') === 0) {
return cookie.substring(key.length + 1);
}
}
return null;
}
/**
* 发送通知
* @param {string} message 通知内容
* @param {string} type 通知类型
*/
function sendNotification(message, type = 'info') {
console.log(`[B站抽奖动态智能处理] ${message}`);
GM_notification({
text: message,
title: 'B站抽奖动态智能处理',
image: 'https://www.bilibili.com/favicon.ico',
timeout: 3000,
});
}
/**
* 获取用户动态列表
* @param {string} uid 用户ID
* @param {string} offset 偏移量
* @returns {Promise} 动态数据
*/
async function getUserDynamics(uid, offset = '') {
const apiUrl = `https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space?offset=${offset}&host_mid=${uid}`;
try {
const response = await axios.get(apiUrl, {
withCredentials: true
});
return response.data;
} catch (error) {
console.error('获取动态失败:', error);
throw error;
}
}
/**
* 获取抽奖状态
* @param {string} lotteryId 抽奖ID
* @returns {Promise} 抽奖状态信息
*/
async function getLotteryStatus(lotteryId) {
const apiUrl = `https://api.vc.bilibili.com/lottery_svr/v1/lottery_svr/lottery_notice?business_type=4&business_id=${lotteryId}`;
try {
const response = await axios.get(apiUrl);
return response.data;
} catch (error) {
console.error('获取抽奖状态失败:', error);
throw error;
}
}
/**
* 验证动态是否被成功删除
* @param {string} dynamicId 动态ID
* @param {string} uid 用户ID
* @returns {Promise<boolean>} 是否删除成功
*/
async function verifyDynamicDeleted(dynamicId, uid) {
try {
// 等待一段时间让服务器处理
await new Promise(resolve => setTimeout(resolve, 2000));
// 重新获取用户动态,检查目标动态是否还存在
const dynamicsData = await getUserDynamics(uid);
if (dynamicsData.code !== 0) {
console.warn('验证删除时获取动态失败:', dynamicsData.message);
return false; // 无法验证,假定删除失败
}
const items = dynamicsData.data.items || [];
const stillExists = items.some(item => item.id_str === dynamicId);
return !stillExists; // 如果不存在则删除成功
} catch (error) {
console.error('验证删除失败:', error);
return false; // 验证失败,假定删除失败
}
}
/**
* 删除动态(带重试和验证机制)
* @param {string} dynamicId 动态ID
* @param {string} uid 用户ID
* @param {number} maxRetries 最大重试次数
* @returns {Promise<Object>} 删除结果
*/
async function deleteDynamicWithRetry(dynamicId, uid, maxRetries = 3) {
const csrf = getCookie('bili_jct');
const apiUrl = `https://api.bilibili.com/x/dynamic/feed/operate/remove?csrf=${csrf}`;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
console.log(`尝试删除动态 ${dynamicId},第 ${attempt} 次`);
const response = await axios.post(apiUrl, {
dyn_id_str: dynamicId
}, {
withCredentials: true
});
if (response.data.code === 0) {
// API返回成功,进行验证
console.log(`删除API调用成功,开始验证动态 ${dynamicId} 是否真的被删除`);
const isDeleted = await verifyDynamicDeleted(dynamicId, uid);
if (isDeleted) {
console.log(`动态 ${dynamicId} 删除成功并验证通过`);
return {
code: 0,
message: '删除成功',
verified: true,
attempts: attempt
};
} else {
console.warn(`动态 ${dynamicId} API返回成功但验证失败,可能触发了验证码或其他限制`);
if (attempt < maxRetries) {
// 增加更长的延迟避免触发验证码
const delayTime = GM_getValue('operation-delay') * attempt;
console.log(`等待 ${delayTime}ms 后重试...`);
await new Promise(resolve => setTimeout(resolve, delayTime));
continue;
} else {
return {
code: -1,
message: '删除验证失败,可能触发了验证码或其他安全限制',
verified: false,
attempts: attempt
};
}
}
} else {
console.warn(`删除动态 ${dynamicId} 失败:`, response.data.message);
if (attempt < maxRetries) {
const delayTime = GM_getValue('operation-delay') * attempt;
console.log(`等待 ${delayTime}ms 后重试...`);
await new Promise(resolve => setTimeout(resolve, delayTime));
continue;
} else {
return {
code: response.data.code,
message: response.data.message || '删除失败',
verified: false,
attempts: attempt
};
}
}
} catch (error) {
console.error(`删除动态 ${dynamicId} 第 ${attempt} 次尝试失败:`, error);
if (attempt < maxRetries) {
const delayTime = GM_getValue('operation-delay') * attempt;
console.log(`等待 ${delayTime}ms 后重试...`);
await new Promise(resolve => setTimeout(resolve, delayTime));
continue;
} else {
return {
code: -1,
message: `删除失败: ${error.message}`,
verified: false,
attempts: attempt
};
}
}
}
return {
code: -1,
message: '删除失败,已达到最大重试次数',
verified: false,
attempts: maxRetries
};
}
/**
* 删除动态(保留原函数以兼容)
* @param {string} dynamicId 动态ID
* @returns {Promise} 删除结果
*/
async function deleteDynamic(dynamicId) {
const csrf = getCookie('bili_jct');
const apiUrl = `https://api.bilibili.com/x/dynamic/feed/operate/remove?csrf=${csrf}`;
try {
const response = await axios.post(apiUrl, {
dyn_id_str: dynamicId
}, {
withCredentials: true
});
return response.data;
} catch (error) {
console.error('删除动态失败:', error);
throw error;
}
}
/**
* 取关用户
* @param {string} uid 用户ID
* @returns {Promise} 取关结果
*/
async function unfollowUser(uid) {
const csrf = getCookie('bili_jct');
const apiUrl = 'https://api.bilibili.com/x/relation/modify';
try {
const response = await axios.post(apiUrl, {
fid: uid,
act: 2, // 2表示取关
re_src: 11,
spmid: '333.999.0.0',
csrf: csrf
}, {
withCredentials: true
});
return response.data;
} catch (error) {
console.error('取关失败:', error);
throw error;
}
}
/**
* 检查用户是否有其他未开奖的抽奖动态
* @param {string} uid 用户ID
* @param {string} excludeId 排除的动态ID
* @returns {Promise<Object>} 返回检查结果 {hasOtherLottery: boolean, otherLotteryCount: number}
*/
async function hasOtherUnfinishedLottery(uid, excludeId) {
try {
let offset = '';
let hasMore = true;
let otherLotteryCount = 0;
while (hasMore) {
const dynamicsData = await getUserDynamics(uid, offset);
if (dynamicsData.code !== 0) {
console.error('获取用户动态失败:', dynamicsData.message);
return {hasOtherLottery: false, otherLotteryCount: 0};
}
const items = dynamicsData.data.items || [];
for (const item of items) {
// 跳过要排除的动态
if (item.id_str === excludeId) continue;
// 检查是否为转发动态且包含抽奖
if (item.orig && item.orig.id_str) {
try {
const lotteryData = await getLotteryStatus(item.orig.id_str);
// 如果是抽奖且未开奖
if (lotteryData.code === 0 && lotteryData.data.status === 0) {
otherLotteryCount++;
}
} catch (error) {
// 不是抽奖动态,继续检查下一个
continue;
}
}
}
// 检查是否还有更多动态
offset = dynamicsData.data.offset;
hasMore = dynamicsData.data.has_more && offset;
// 为了避免请求过于频繁,添加延迟
await new Promise(resolve => setTimeout(resolve, 500));
}
return {hasOtherLottery: otherLotteryCount > 0, otherLotteryCount};
} catch (error) {
console.error('检查其他抽奖动态失败:', error);
return {hasOtherLottery: false, otherLotteryCount: 0};
}
}
/**
* 处理单个抽奖动态
* @param {Object} dynamic 动态对象
* @param {Function} progressCallback 进度回调
* @param {string} currentUid 当前用户ID(用于删除验证)
* @returns {Promise<Object>} 处理结果
*/
async function processLotteryDynamic(dynamic, progressCallback, currentUid) {
const result = {
dynamicId: dynamic.id_str,
deleted: false,
unfollowed: false,
reason: '',
authorName: '',
authorUid: '',
lotteryStatus: null,
isLottery: false,
otherLotteryCount: 0,
deleteAttempts: 0,
deleteVerified: false
};
try {
// 检查是否为转发的抽奖动态
if (!dynamic.orig || !dynamic.orig.id_str) {
result.reason = '不是转发动态';
return result;
}
const origId = dynamic.orig.id_str;
const authorInfo = dynamic.orig.modules?.module_author;
if (authorInfo) {
result.authorName = authorInfo.name;
result.authorUid = authorInfo.mid;
}
progressCallback(`正在检查抽奖状态: ${result.authorName || '未知用户'}`);
// 获取抽奖状态
const lotteryData = await getLotteryStatus(origId);
if (lotteryData.code !== 0) {
result.reason = '不是抽奖动态';
return result;
}
result.isLottery = true;
result.lotteryStatus = lotteryData.data.status;
// 0: 未开奖, 2: 已开奖
if (result.lotteryStatus === 0) {
result.reason = '抽奖未开奖,保留';
return result;
}
if (result.lotteryStatus === 2) {
progressCallback(`抽奖已开奖,准备删除动态并验证...`);
// 删除已开奖的动态(使用带重试和验证的版本)
if (GM_getValue('delete-finished-lottery')) {
const maxRetries = GM_getValue('delete-retry-count');
const deleteResult = await deleteDynamicWithRetry(dynamic.id_str, currentUid, maxRetries);
result.deleteAttempts = deleteResult.attempts;
result.deleteVerified = deleteResult.verified;
if (deleteResult.code === 0 && deleteResult.verified) {
result.deleted = true;
result.reason = `已开奖,动态已删除并验证成功 (尝试${deleteResult.attempts}次)`;
} else {
result.reason = `删除失败: ${deleteResult.message} (尝试${deleteResult.attempts}次)`;
// 如果删除失败,不继续处理取关逻辑
return result;
}
}
// 检查是否需要取关(只检查已关注且参与过抽奖的UP主)
if (GM_getValue('smart-unfollow') && authorInfo && authorInfo.following) {
progressCallback(`检查UP主是否有其他抽奖...`);
const otherLotteryCheck = await hasOtherUnfinishedLottery(result.authorUid, origId);
result.otherLotteryCount = otherLotteryCheck.otherLotteryCount;
if (!otherLotteryCheck.hasOtherLottery) {
// 没有其他未开奖抽奖,可以取关
progressCallback(`准备取关UP主: ${result.authorName}`);
const unfollowResult = await unfollowUser(result.authorUid);
if (unfollowResult.code === 0) {
result.unfollowed = true;
result.reason += ',已取关UP主';
} else {
result.reason += ',取关失败';
}
} else {
result.reason += `,UP主还有${result.otherLotteryCount}个抽奖,保持关注`;
}
}
}
} catch (error) {
console.error('处理动态失败:', error);
result.reason = `处理失败: ${error.message}`;
}
return result;
}
/**
* 主处理函数
*/
async function processAllLotteryDynamics() {
// 检查是否正在处理
if (isProcessing) {
sendNotification('已有处理任务在进行中,请等待完成后再试');
return;
}
const uid = getCookie('DedeUserID');
if (!uid) {
sendNotification('未检测到登录状态');
return;
}
// 设置处理状态
isProcessing = true;
currentProcessId = Date.now().toString();
// 显示进度弹窗
showProgressDialog();
try {
updateProgress(0, '正在获取动态列表...');
let allDynamics = [];
let offset = '';
let hasMore = true;
// 获取所有动态
while (hasMore) {
const dynamicsData = await getUserDynamics(uid, offset);
if (dynamicsData.code !== 0) {
throw new Error(`获取动态失败: ${dynamicsData.message}`);
}
const items = dynamicsData.data.items || [];
allDynamics = allDynamics.concat(items);
offset = dynamicsData.data.offset;
hasMore = dynamicsData.data.has_more && offset;
updateProgress(10, `已获取 ${allDynamics.length} 条动态...`);
// 添加延迟避免请求过频
await new Promise(resolve => setTimeout(resolve, 500));
}
// 筛选出转发的抽奖动态
const lotteryDynamics = [];
const totalDynamics = allDynamics.length;
updateProgress(20, '正在识别抽奖动态...');
for (let i = 0; i < allDynamics.length; i++) {
const dynamic = allDynamics[i];
if (dynamic.orig && dynamic.orig.id_str) {
try {
const lotteryData = await getLotteryStatus(dynamic.orig.id_str);
if (lotteryData.code === 0) {
lotteryDynamics.push(dynamic);
}
} catch (error) {
// 不是抽奖动态,跳过
}
}
// 更新识别进度
if (i % 10 === 0) {
const identifyProgress = 20 + (i / allDynamics.length) * 10;
updateProgress(identifyProgress, `正在识别抽奖动态... ${i}/${allDynamics.length}`);
}
}
updateProgress(30, `找到 ${lotteryDynamics.length} 条抽奖动态`);
if (lotteryDynamics.length === 0) {
const finalResult = {
totalDynamics,
lotteryDynamics: 0,
deletedCount: 0,
keptCount: 0,
deleteFailedCount: 0,
unfollowedUsers: [],
keptFollowUsers: [],
results: []
};
updateProgress(100, '没有找到抽奖动态');
showDetailedResults(finalResult);
return;
}
// 处理每个抽奖动态
const results = [];
for (let i = 0; i < lotteryDynamics.length; i++) {
const dynamic = lotteryDynamics[i];
const progress = 30 + (i / lotteryDynamics.length) * 60;
const result = await processLotteryDynamic(dynamic, (status) => {
updateProgress(progress, status);
}, uid);
results.push(result);
// 增加更长的延迟避免触发验证码
const delayTime = GM_getValue('operation-delay');
await new Promise(resolve => setTimeout(resolve, delayTime));
}
// 统计结果
const finalResult = analyzeResults(results, totalDynamics, lotteryDynamics.length);
// 显示详细处理结果
updateProgress(100, '处理完成');
showDetailedResults(finalResult);
} catch (error) {
console.error('处理失败:', error);
updateProgress(100, `处理失败: ${error.message}`);
setTimeout(() => {
hideProgressDialog();
isProcessing = false;
}, 3000);
}
}
/**
* 分析处理结果
* @param {Array} results 处理结果数组
* @param {number} totalDynamics 总动态数
* @param {number} lotteryDynamics 抽奖动态数
* @returns {Object} 统计结果
*/
function analyzeResults(results, totalDynamics, lotteryDynamics) {
const deletedCount = results.filter(r => r.deleted).length;
const keptCount = results.filter(r => r.isLottery && r.lotteryStatus === 0).length;
const deleteFailedCount = results.filter(r => r.lotteryStatus === 2 && !r.deleted).length;
// 统计取关的用户
const unfollowedUsers = results
.filter(r => r.unfollowed)
.map(r => ({
name: r.authorName,
uid: r.authorUid
}));
// 统计删除了动态但保持关注的用户
const keptFollowUsers = results
.filter(r => r.deleted && !r.unfollowed && r.authorUid && r.otherLotteryCount > 0)
.map(r => ({
name: r.authorName,
uid: r.authorUid,
otherLotteryCount: r.otherLotteryCount
}));
// 去重
const uniqueUnfollowed = unfollowedUsers.filter((user, index, self) =>
index === self.findIndex(u => u.uid === user.uid)
);
const uniqueKeptFollow = keptFollowUsers.filter((user, index, self) =>
index === self.findIndex(u => u.uid === user.uid)
);
return {
totalDynamics,
lotteryDynamics,
deletedCount,
keptCount,
deleteFailedCount,
unfollowedUsers: uniqueUnfollowed,
keptFollowUsers: uniqueKeptFollow,
results
};
}
/**
* 显示进度对话框
*/
function showProgressDialog() {
// 先隐藏小窗口
hideMiniProgress();
const dialog = document.createElement('div');
dialog.className = 'lottery-handler-popup';
dialog.id = 'lottery-progress-dialog';
dialog.innerHTML = `
<div class="lottery-handler-content">
<div class="lottery-handler-header">
<span>处理抽奖动态</span>
<button class="minimize-btn" id="minimize-btn" title="最小化">−</button>
</div>
<div class="lottery-handler-body">
<div class="progress-bar">
<div class="progress-fill" id="progress-fill" style="width: 0%"></div>
</div>
<div class="status-text" id="status-text">准备开始...</div>
</div>
<div class="lottery-handler-footer">
<button class="btn btn-secondary" id="cancel-btn">取消</button>
</div>
</div>
`;
// 绑定事件监听器
const minimizeBtn = dialog.querySelector('#minimize-btn');
const cancelBtn = dialog.querySelector('#cancel-btn');
if (minimizeBtn) {
minimizeBtn.addEventListener('click', minimizeProgressDialog);
}
if (cancelBtn) {
cancelBtn.addEventListener('click', cancelProcess);
}
document.body.appendChild(dialog);
}
/**
* 最小化进度对话框
*/
function minimizeProgressDialog() {
hideProgressDialog();
showMiniProgress();
}
/**
* 显示小窗口进度
*/
function showMiniProgress() {
let miniProgress = document.getElementById('mini-progress');
if (!miniProgress) {
miniProgress = document.createElement('div');
miniProgress.className = 'mini-progress';
miniProgress.id = 'mini-progress';
miniProgress.innerHTML = `
<div class="mini-header">
<span>处理中...</span>
<button class="expand-btn" id="expand-btn" title="展开">+</button>
</div>
<div class="progress-bar">
<div class="progress-fill" id="mini-progress-fill" style="width: 0%"></div>
</div>
<div class="status-text" id="mini-status-text">准备开始...</div>
`;
// 绑定展开按钮事件
const expandBtn = miniProgress.querySelector('#expand-btn');
if (expandBtn) {
expandBtn.addEventListener('click', expandProgressDialog);
}
document.body.appendChild(miniProgress);
}
miniProgress.classList.add('show');
}
/**
* 隐藏小窗口进度
*/
function hideMiniProgress() {
const miniProgress = document.getElementById('mini-progress');
if (miniProgress) {
miniProgress.classList.remove('show');
}
}
/**
* 展开进度对话框
*/
function expandProgressDialog() {
hideMiniProgress();
showProgressDialog();
}
/**
* 取消处理
*/
function cancelProcess() {
if (isProcessing) {
const confirmed = confirm('确定要取消当前处理任务吗?');
if (confirmed) {
isProcessing = false;
hideProgressDialog();
hideMiniProgress();
sendNotification('处理任务已取消');
}
} else {
hideProgressDialog();
}
}
/**
* 更新进度
* @param {number} percent 进度百分比
* @param {string} status 状态文本
*/
function updateProgress(percent, status) {
// 更新主窗口进度
const progressFill = document.getElementById('progress-fill');
const statusText = document.getElementById('status-text');
if (progressFill) {
progressFill.style.width = `${percent}%`;
}
if (statusText) {
statusText.textContent = status;
}
// 同时更新小窗口进度
const miniProgressFill = document.getElementById('mini-progress-fill');
const miniStatusText = document.getElementById('mini-status-text');
if (miniProgressFill) {
miniProgressFill.style.width = `${percent}%`;
}
if (miniStatusText) {
miniStatusText.textContent = status;
}
}
/**
* 隐藏进度对话框
*/
function hideProgressDialog() {
const dialog = document.getElementById('lottery-progress-dialog');
if (dialog) {
document.body.removeChild(dialog);
}
}
/**
* 显示详细处理结果
* @param {Object} finalResult 最终结果统计
*/
function showDetailedResults(finalResult) {
// 重置处理状态
isProcessing = false;
// 隐藏进度窗口和小窗口
hideProgressDialog();
hideMiniProgress();
const {
totalDynamics,
lotteryDynamics,
deletedCount,
keptCount,
deleteFailedCount,
unfollowedUsers,
keptFollowUsers,
results
} = finalResult;
const dialog = document.createElement('div');
dialog.className = 'lottery-handler-popup';
dialog.id = 'lottery-result-dialog';
// 统计删除失败的动态
const failedDynamics = results.filter(r => r.lotteryStatus === 2 && !r.deleted);
dialog.innerHTML = `
<div class="lottery-handler-content large">
<div class="lottery-handler-header">
<span>处理完成 - 详细结果</span>
</div>
<div class="lottery-handler-body">
<div class="result-section">
<div class="result-title">📊 统计概览</div>
<div class="result-stats">
<div class="stat-item">
<div class="stat-number">${totalDynamics}</div>
<div class="stat-label">总动态数</div>
</div>
<div class="stat-item">
<div class="stat-number">${lotteryDynamics}</div>
<div class="stat-label">抽奖动态</div>
</div>
<div class="stat-item">
<div class="stat-number">${deletedCount}</div>
<div class="stat-label">成功删除</div>
</div>
<div class="stat-item">
<div class="stat-number">${keptCount}</div>
<div class="stat-label">保留未开奖</div>
</div>
${deleteFailedCount > 0 ? `
<div class="stat-item" style="border-color: #ff4d4f;">
<div class="stat-number" style="color: #ff4d4f;">${deleteFailedCount}</div>
<div class="stat-label">删除失败</div>
</div>
` : ''}
</div>
</div>
${deleteFailedCount > 0 ? `
<div class="result-section">
<div class="result-title">⚠️ 删除失败的动态 (${deleteFailedCount})</div>
<div style="font-size: 12px; color: #666; margin-bottom: 10px;">
以下动态删除失败,可能触发了验证码或其他安全限制
</div>
<div class="user-list">
${failedDynamics.map(result => `
<div class="user-item">
<span class="user-name">${result.authorName || '未知用户'}</span>
<span class="user-uid">动态ID: ${result.dynamicId}</span>
<span style="color: #ff4d4f; font-size: 12px; margin-left: auto;">
${result.reason}
</span>
</div>
`).join('')}
</div>
</div>
` : ''}
<div class="result-section">
<div class="result-title">🚫 已取关UP主 (${unfollowedUsers.length})</div>
<div class="user-list">
${unfollowedUsers.length > 0 ?
unfollowedUsers.map(user => `
<div class="user-item">
<span class="user-name">${user.name}</span>
<span class="user-uid">UID: ${user.uid}</span>
</div>
`).join('') :
'<div class="no-data">没有取关任何UP主</div>'
}
</div>
</div>
<div class="result-section">
<div class="result-title">💝 保持关注UP主 (${keptFollowUsers.length})</div>
<div style="font-size: 12px; color: #666; margin-bottom: 10px;">
以下UP主的抽奖动态已删除,但因还有其他未开奖抽奖而保持关注
</div>
<div class="user-list">
${keptFollowUsers.length > 0 ?
keptFollowUsers.map(user => `
<div class="user-item">
<span class="user-name">${user.name}</span>
<span class="user-uid">UID: ${user.uid}</span>
<span style="color: #00a1d6; font-size: 12px; margin-left: auto;">
还有${user.otherLotteryCount}个抽奖
</span>
</div>
`).join('') :
'<div class="no-data">没有此类UP主</div>'
}
</div>
</div>
</div>
<div class="lottery-handler-footer">
<button class="btn btn-secondary" id="export-btn">导出详情</button>
<button class="btn btn-success" id="finish-btn">完成</button>
</div>
</div>
`;
document.body.appendChild(dialog);
// 绑定事件监听器
const exportBtn = dialog.querySelector('#export-btn');
const finishBtn = dialog.querySelector('#finish-btn');
if (exportBtn) {
exportBtn.addEventListener('click', () => {
exportResults(btoa(JSON.stringify(results)));
});
}
if (finishBtn) {
finishBtn.addEventListener('click', hideResultDialog);
}
// 发送通知
const notificationMsg = deleteFailedCount > 0
? `处理完成!成功删除 ${deletedCount} 条,失败 ${deleteFailedCount} 条,取关 ${unfollowedUsers.length} 个UP主`
: `处理完成!删除 ${deletedCount} 条动态,取关 ${unfollowedUsers.length} 个UP主`;
sendNotification(notificationMsg);
// 详细结果输出到控制台
console.log('📊 处理完成,详细结果:', finalResult);
}
/**
* 隐藏结果对话框
*/
function hideResultDialog() {
const dialog = document.getElementById('lottery-result-dialog');
if (dialog) {
document.body.removeChild(dialog);
}
}
/**
* 导出处理结果
* @param {string} encodedResults Base64编码的结果数据
*/
function exportResults(encodedResults) {
try {
const results = JSON.parse(atob(encodedResults));
const exportData = {
timestamp: new Date().toLocaleString(),
summary: {
totalProcessed: results.length,
deleted: results.filter(r => r.deleted).length,
kept: results.filter(r => r.isLottery && r.lotteryStatus === 0).length,
unfollowed: results.filter(r => r.unfollowed).length
},
details: results.map(r => ({
动态ID: r.dynamicId,
UP主: r.authorName,
UP主UID: r.authorUid,
抽奖状态: r.lotteryStatus === 0 ? '未开奖' : (r.lotteryStatus === 2 ? '已开奖' : '未知'),
是否删除: r.deleted ? '是' : '否',
是否取关: r.unfollowed ? '是' : '否',
其他抽奖数量: r.otherLotteryCount || 0,
处理结果: r.reason
}))
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `B站抽奖处理结果_${new Date().toISOString().slice(0, 10)}.json`;
a.click();
URL.revokeObjectURL(url);
sendNotification('结果已导出到文件');
} catch (error) {
console.error('导出失败:', error);
sendNotification('导出失败,请查看控制台');
}
}
/**
* 显示设置对话框
*/
function showSettingsDialog() {
// 检查是否正在处理,如果是则提示用户
if (isProcessing) {
alert('正在处理抽奖动态,请等待完成后再修改设置。');
return;
}
// 检查是否已有设置对话框打开
if (document.getElementById('lottery-settings-dialog')) {
return;
}
const dialog = document.createElement('div');
dialog.className = 'lottery-handler-popup';
dialog.id = 'lottery-settings-dialog';
dialog.innerHTML = `
<div class="lottery-handler-content">
<div class="lottery-handler-header">
<span>设置</span>
</div>
<div class="lottery-handler-body">
<div class="config-item">
<label class="config-label">智能取关功能</label>
<input type="checkbox" class="config-input" id="smart-unfollow" ${GM_getValue('smart-unfollow') ? 'checked' : ''} ${isProcessing ? 'disabled' : ''}>
</div>
<div class="config-item">
<label class="config-label">删除已开奖动态</label>
<input type="checkbox" class="config-input" id="delete-finished-lottery" ${GM_getValue('delete-finished-lottery') ? 'checked' : ''} ${isProcessing ? 'disabled' : ''}>
</div>
<div class="config-item">
<label class="config-label">删除重试次数</label>
<input type="number" class="config-input" id="delete-retry-count" value="${GM_getValue('delete-retry-count')}" min="1" max="5" ${isProcessing ? 'disabled' : ''}>
</div>
<div class="config-item">
<label class="config-label">操作延迟(毫秒)</label>
<input type="number" class="config-input" id="operation-delay" value="${GM_getValue('operation-delay')}" min="1000" max="10000" step="500" ${isProcessing ? 'disabled' : ''}>
</div>
<div style="margin-top: 15px; padding: 10px; background-color: #f5f5f5; border-radius: 5px; font-size: 12px; color: #666;">
<p><strong>⚠️ 安全提醒:</strong></p>
<p>• 智能取关:仅取关参与过抽奖且无其他未开奖抽奖的UP主</p>
<p>• 删除验证机制:删除后会验证是否真的删除成功</p>
<p>• 删除重试:如果删除失败会自动重试指定次数</p>
<p>• 操作延迟:每次操作间隔时间,避免触发验证码</p>
<p>• 建议延迟设置为1500毫秒以上</p>
</div>
</div>
<div class="lottery-handler-footer">
<button class="btn btn-secondary" id="cancel-settings-btn" ${isProcessing ? 'disabled' : ''}>取消</button>
<button class="btn btn-primary" id="save-settings-btn" ${isProcessing ? 'disabled' : ''}>保存</button>
</div>
</div>
`;
document.body.appendChild(dialog);
// 绑定事件监听器
const cancelBtn = dialog.querySelector('#cancel-settings-btn');
const saveBtn = dialog.querySelector('#save-settings-btn');
if (cancelBtn) {
cancelBtn.addEventListener('click', hideSettingsDialog);
}
if (saveBtn) {
saveBtn.addEventListener('click', saveSettings);
}
}
/**
* 隐藏设置对话框
*/
function hideSettingsDialog() {
const dialog = document.getElementById('lottery-settings-dialog');
if (dialog) {
document.body.removeChild(dialog);
}
}
/**
* 保存设置
*/
function saveSettings() {
const smartUnfollow = document.getElementById('smart-unfollow').checked;
const deleteFinishedLottery = document.getElementById('delete-finished-lottery').checked;
const deleteRetryCount = parseInt(document.getElementById('delete-retry-count').value);
const operationDelay = parseInt(document.getElementById('operation-delay').value);
// 验证设置值
if (deleteRetryCount < 1 || deleteRetryCount > 5) {
alert('删除重试次数必须在1-5之间');
return;
}
if (operationDelay < 1000 || operationDelay > 10000) {
alert('操作延迟必须在1000-10000毫秒之间');
return;
}
GM_setValue('smart-unfollow', smartUnfollow);
GM_setValue('delete-finished-lottery', deleteFinishedLottery);
GM_setValue('delete-retry-count', deleteRetryCount);
GM_setValue('operation-delay', operationDelay);
sendNotification('设置已保存');
hideSettingsDialog();
}
/**
* 初始化
*/
function init() {
initConfig();
// 将所有需要在HTML onclick中使用的函数添加到全局作用域
window.hideProgressDialog = hideProgressDialog;
window.minimizeProgressDialog = minimizeProgressDialog;
window.expandProgressDialog = expandProgressDialog;
window.cancelProcess = cancelProcess;
window.hideResultDialog = hideResultDialog;
window.exportResults = exportResults;
window.hideSettingsDialog = hideSettingsDialog;
window.saveSettings = saveSettings;
// 注册菜单命令
GM_registerMenuCommand('开始智能处理', processAllLotteryDynamics);
GM_registerMenuCommand('设置', showSettingsDialog);
sendNotification('B站抽奖动态智能处理脚本已加载');
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();