// ==UserScript==
// @name Bilibili评论展开助手
// @namespace http://tampermonkey.net/
// @version 2.3.1
// @description 智能展开Bilibili评论回复,一键查看所有子评论,支持按热度和时间排序,提供流畅的评论浏览体验
// @author Rygtx
// @icon https://www.bilibili.com/favicon.ico
// @match https://www.bilibili.com/video/*
// @grant none
// @license CC-BY-NC-4.0
// @run-at document-end
// @downloadURL https://update.greasyfork.icu/scripts/550681/Bilibili%E8%AF%84%E8%AE%BA%E5%B1%95%E5%BC%80%E5%8A%A9%E6%89%8B.user.js
// @updateURL https://update.greasyfork.icu/scripts/550681/Bilibili%E8%AF%84%E8%AE%BA%E5%B1%95%E5%BC%80%E5%8A%A9%E6%89%8B.meta.js
// ==/UserScript==
(function () {
'use strict';
// 配置常量
const CONFIG = {
API_BASE: 'https://api.bilibili.com',
COMMENT_TYPE: 1, // 视频评论类型
MAX_RETRIES: 3,
RETRY_DELAY: 1000,
REQUEST_TIMEOUT: 10000
};
// 工具函数
const Utils = {
// 调试日志输出功能
log(level, message, ...args) {
const timestamp = new Date().toISOString();
const prefix = `[Bilibili评论展开助手 ${timestamp}]`;
switch (level) {
case 'error':
console.error(prefix, message, ...args);
break;
case 'warn':
console.warn(prefix, message, ...args);
break;
case 'info':
console.info(prefix, message, ...args);
break;
default:
console.log(prefix, message, ...args);
}
},
formatTime(timestamp) {
if (!timestamp) return '未知时间';
const date = new Date(timestamp * 1000);
const now = new Date();
const diff = now - date;
const minute = 60 * 1000;
const hour = 60 * minute;
const day = 24 * hour;
const month = 30 * day;
const year = 365 * day;
if (diff < minute) {
return '刚刚';
} else if (diff < hour) {
return `${Math.floor(diff / minute)}分钟前`;
} else if (diff < day) {
return `${Math.floor(diff / hour)}小时前`;
} else if (diff < month) {
return `${Math.floor(diff / day)}天前`;
} else if (diff < year) {
return `${Math.floor(diff / month)}个月前`;
} else {
return `${Math.floor(diff / year)}年前`;
}
},
formatDetailedTime(timestamp) {
if (!timestamp) return '未知时间';
const date = new Date(timestamp * 1000);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
},
// HTML转义工具函数
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
},
// 获取视频标题
async getVideoTitle(videoId, isAv = false) {
try {
let apiUrl;
if (isAv) {
apiUrl = `https://api.bilibili.com/x/web-interface/view?aid=${videoId}`;
} else {
apiUrl = `https://api.bilibili.com/x/web-interface/view?bvid=${videoId}`;
}
const response = await fetch(apiUrl, {
method: 'GET',
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (data.code === 0 && data.data && data.data.title) {
return data.data.title;
}
throw new Error('API返回错误');
} catch (error) {
return null;
}
},
// 处理评论内容中的视频链接 - 增强版
async processCommentContentEnhanced(content) {
if (!content) return '内容为空';
const escapedContent = this.escapeHtml(content);
// 识别av号和BV号的正则表达式
const avPattern = /\b(av)(\d+)\b/gi;
let processedContent = escapedContent;
const videoPromises = [];
// 收集所有视频链接
const videoMatches = [];
// 处理av号
let match;
while ((match = avPattern.exec(escapedContent)) !== null) {
videoMatches.push({
match: match[0],
type: 'av',
id: match[2],
fullMatch: match[0]
});
}
// 处理BV号
const bvRegex = /\b(BV[a-zA-Z0-9]+)\b/gi;
while ((match = bvRegex.exec(escapedContent)) !== null) {
videoMatches.push({
match: match[0],
type: 'bv',
id: match[0],
fullMatch: match[0]
});
}
// 为每个视频获取标题
for (const video of videoMatches) {
const titlePromise = this.getVideoTitle(video.id, video.type === 'av')
.then(title => ({ ...video, title }));
videoPromises.push(titlePromise);
}
// 等待所有标题获取完成
const videoData = await Promise.all(videoPromises);
// 替换视频链接
for (const video of videoData) {
const url = video.type === 'av'
? `https://www.bilibili.com/video/av${video.id}/`
: `https://www.bilibili.com/video/${video.id}/`;
const title = video.title || video.fullMatch;
const linkHtml = this.createVideoLinkHtml(url, title, video.fullMatch);
processedContent = processedContent.replace(video.fullMatch, linkHtml);
}
return processedContent;
},
// 创建B站风格的视频链接HTML - 暗色主题优化
createVideoLinkHtml(url, title) {
// 限制标题长度,避免过长
const displayTitle = title.length > 30 ? title.substring(0, 30) + '...' : title;
return `
`;
},
// 为增强版视频链接添加悬停效果 - 暗色主题优化
addEnhancedVideoLinkHoverEffects(container) {
const videoLinks = container.querySelectorAll('.bili-video-link-enhanced');
videoLinks.forEach(link => {
link.addEventListener('mouseover', () => {
link.style.background = 'rgba(0,161,214,0.15)';
link.style.borderColor = 'rgba(0,161,214,0.4)';
link.style.transform = 'translateY(-1px)';
link.style.boxShadow = '0 2px 8px rgba(0,161,214,0.2)';
const span = link.querySelector('span');
if (span) span.style.color = '#40a9ff';
const img = link.querySelector('img');
if (img) img.style.filter = 'brightness(1.3)';
});
link.addEventListener('mouseout', () => {
link.style.background = 'rgba(0,161,214,0.08)';
link.style.borderColor = 'rgba(0,161,214,0.2)';
link.style.transform = 'translateY(0)';
link.style.boxShadow = 'none';
const span = link.querySelector('span');
if (span) span.style.color = '#00a1d6';
const img = link.querySelector('img');
if (img) img.style.filter = 'brightness(1.1)';
});
// 点击效果
link.addEventListener('mousedown', () => {
link.style.transform = 'translateY(0) scale(0.98)';
});
link.addEventListener('mouseup', () => {
link.style.transform = 'translateY(-1px) scale(1)';
});
});
},
// 为简化版视频链接添加悬停效果
addVideoLinkHoverEffects(container) {
const videoLinks = container.querySelectorAll('.bili-video-link-simple');
videoLinks.forEach(link => {
link.addEventListener('mouseover', () => {
link.style.borderBottomColor = '#00a1d6';
link.style.color = '#40a9ff';
link.style.background = 'rgba(0,161,214,0.1)';
});
link.addEventListener('mouseout', () => {
link.style.borderBottomColor = 'transparent';
link.style.color = '#00a1d6';
link.style.background = 'rgba(0,161,214,0.05)';
});
});
},
formatNumber(num) {
if (num >= 10000) {
return `${(num / 10000).toFixed(1)}万`;
}
return num.toString();
},
async sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
},
async fetchWithRetry(url, options = {}, retries = CONFIG.MAX_RETRIES) {
for (let i = 0; i < retries; i++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), CONFIG.REQUEST_TIMEOUT);
const response = await fetch(url, {
...options,
signal: controller.signal,
headers: {
'User-Agent': navigator.userAgent,
'Referer': window.location.href,
...options.headers
}
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response;
} catch (error) {
if (i === retries - 1) {
throw error;
}
await Utils.sleep(CONFIG.RETRY_DELAY * (i + 1));
}
}
}
};
// B站评论API组件
class BilibiliCommentAPI {
constructor() {
this.cache = new Map();
this.cacheExpiry = 5 * 60 * 1000; // 5分钟缓存
}
getCacheKey(type, oid, rpid = null, page = 1) {
return `${type}_${oid}_${rpid || 'root'}_${page}`;
}
isValidCache(cacheItem) {
return cacheItem && (Date.now() - cacheItem.timestamp) < this.cacheExpiry;
}
async getCommentReplies(oid, rootRpid, page = 1, pageSize = 20) {
const cacheKey = this.getCacheKey('replies', oid, rootRpid, page);
const cached = this.cache.get(cacheKey);
if (this.isValidCache(cached)) {
Utils.log('info', `使用缓存数据: ${cacheKey}`);
return cached.data;
}
try {
const url = new URL(`${CONFIG.API_BASE}/x/v2/reply/reply`);
url.searchParams.set('type', CONFIG.COMMENT_TYPE);
url.searchParams.set('oid', oid);
url.searchParams.set('root', rootRpid);
url.searchParams.set('ps', pageSize);
url.searchParams.set('pn', page);
Utils.log('info', `请求评论回复: ${url.toString()}`);
const response = await Utils.fetchWithRetry(url.toString());
const data = await response.json();
if (data.code !== 0) {
throw new Error(`API错误: ${data.message || '未知错误'} (code: ${data.code})`);
}
// 缓存结果
this.cache.set(cacheKey, {
data: data.data,
timestamp: Date.now()
});
Utils.log('info', `成功获取评论回复: ${data.data?.replies?.length || 0} 条`);
return data.data;
} catch (error) {
Utils.log('error', '获取评论回复失败:', error);
throw error;
}
}
async getAllReplies(oid, rootRpid, maxPages = 10) {
const allReplies = [];
let page = 1;
let hasMore = true;
while (hasMore && page <= maxPages) {
try {
const data = await this.getCommentReplies(oid, rootRpid, page);
if (data?.replies && data.replies.length > 0) {
allReplies.push(...data.replies);
// 检查是否还有更多页
const pageInfo = data.page;
hasMore = pageInfo && page < Math.ceil(pageInfo.count / pageInfo.size);
page++;
// 避免请求过快
if (hasMore) {
await Utils.sleep(500);
}
} else {
hasMore = false;
}
} catch (error) {
Utils.log('error', `获取第${page}页回复失败:`, error);
hasMore = false;
}
}
Utils.log('info', `总共获取到 ${allReplies.length} 条回复`);
return allReplies;
}
async getCommentInfo(oid, rpid) {
const cacheKey = this.getCacheKey('info', oid, rpid);
const cached = this.cache.get(cacheKey);
if (this.isValidCache(cached)) {
return cached.data;
}
try {
const url = new URL(`${CONFIG.API_BASE}/x/v2/reply/info`);
url.searchParams.set('type', CONFIG.COMMENT_TYPE);
url.searchParams.set('oid', oid);
url.searchParams.set('rpid', rpid);
const response = await Utils.fetchWithRetry(url.toString());
const data = await response.json();
if (data.code !== 0) {
throw new Error(`API错误: ${data.message || '未知错误'} (code: ${data.code})`);
}
this.cache.set(cacheKey, {
data: data.data,
timestamp: Date.now()
});
return data.data;
} catch (error) {
throw error;
}
}
clearCache() {
this.cache.clear();
Utils.log('info', 'API缓存已清空');
}
}
// DOM监听器组件
class DOMWatcher {
constructor() {
this.observer = null;
this.isObserving = false;
this.viewMoreButtons = new Set();
this.onViewMoreClick = null;
this.intervalId = null;
this.scanInterval = 1000; // 增加扫描频率到1秒
}
observeCommentSection() {
if (this.isObserving) {
Utils.log('warn', 'DOM监听器已在运行');
return;
}
this.observer = new MutationObserver((mutations) => {
let shouldRescan = false;
mutations.forEach((mutation) => {
// 检查是否有节点被添加或移除
if (mutation.type === 'childList') {
// 检查是否涉及评论相关的DOM变化
const hasCommentChanges = Array.from(mutation.addedNodes).some(node =>
node.nodeType === Node.ELEMENT_NODE &&
(node.tagName === 'BILI-COMMENT-THREAD-RENDERER' ||
node.querySelector && node.querySelector('bili-comment-thread-renderer'))
) || Array.from(mutation.removedNodes).some(node =>
node.nodeType === Node.ELEMENT_NODE &&
(node.tagName === 'BILI-COMMENT-THREAD-RENDERER' ||
node.querySelector && node.querySelector('bili-comment-thread-renderer'))
);
if (hasCommentChanges) {
shouldRescan = true;
}
}
});
if (shouldRescan) {
// 延迟一点时间让DOM完全更新
setTimeout(() => {
this.scanForViewMoreButtons();
this.reattachMissingButtons();
}, 100);
}
});
const targetNode = document.body;
const config = {
childList: true,
subtree: true,
attributes: false
};
this.observer.observe(targetNode, config);
this.isObserving = true;
this.startPeriodicScan();
this.scanForViewMoreButtons();
Utils.log('info', 'DOM监听器已启动 - 增强版');
}
startPeriodicScan() {
if (this.intervalId) {
clearInterval(this.intervalId);
}
this.intervalId = setInterval(() => {
this.scanForViewMoreButtons();
this.reattachMissingButtons();
}, this.scanInterval);
Utils.log('info', `定时检测已启动,每 ${this.scanInterval}ms 检测一次`);
}
// 重新附加丢失的瀑布流按钮
reattachMissingButtons() {
try {
const commentApp = document.querySelector("#commentapp > bili-comments");
if (!commentApp || !commentApp.shadowRoot) return;
const threadRenderers = commentApp.shadowRoot.querySelectorAll("#feed > bili-comment-thread-renderer");
let reattachedCount = 0;
threadRenderers.forEach((threadRenderer) => {
if (!threadRenderer.shadowRoot) return;
const repliesRenderer = threadRenderer.shadowRoot.querySelector("#replies > bili-comment-replies-renderer");
if (!repliesRenderer || !repliesRenderer.shadowRoot) return;
const viewMoreButton = repliesRenderer.shadowRoot.querySelector("#view-more > bili-text-button");
if (!viewMoreButton || !viewMoreButton.shadowRoot) return;
// 检查是否已有评论展开按钮
const existingExpandBtn = this.findExpandButton(threadRenderer);
if (!existingExpandBtn) {
// 如果没有评论展开按钮,重新添加
const button = viewMoreButton.shadowRoot.querySelector("button");
if (button && this.viewMoreButtons.has(button)) {
// 这个按钮之前已经处理过,但评论展开按钮丢失了
const commentInfo = this.extractCommentInfo(viewMoreButton, threadRenderer);
if (commentInfo) {
this.addExpandButtonToStableLocation(threadRenderer, commentInfo);
reattachedCount++;
}
}
}
});
if (reattachedCount > 0) {
Utils.log('info', `重新附加了 ${reattachedCount} 个评论展开按钮`);
}
} catch (error) {
Utils.log('error', '重新附加按钮时出错:', error);
}
}
// 查找评论展开按钮(优化版)
findExpandButton(threadRenderer) {
try {
// 优先在主要位置查找(基于实际使用情况优化)
const primaryLocations = [
threadRenderer.shadowRoot?.querySelector("#replies"),
threadRenderer
];
for (const location of primaryLocations) {
if (location) {
// 直接查找按钮
const expandBtn = location.querySelector?.('.bili-comment-expand-btn');
if (expandBtn) {
return expandBtn;
}
// 检查shadowRoot
if (location.shadowRoot) {
const expandBtn = location.shadowRoot.querySelector('.bili-comment-expand-btn');
if (expandBtn) {
return expandBtn;
}
}
}
}
return null;
} catch (error) {
return null;
}
}
scanForViewMoreButtons() {
try {
const commentApp = document.querySelector("#commentapp > bili-comments");
if (!commentApp || !commentApp.shadowRoot) {
Utils.log('warn', '未找到评论区或shadowRoot');
return;
}
const threadRenderers = commentApp.shadowRoot.querySelectorAll("#feed > bili-comment-thread-renderer");
Utils.log('info', `扫描评论区: 找到 ${threadRenderers.length} 个评论线程`);
let newButtonsFound = 0;
threadRenderers.forEach((threadRenderer) => {
if (!threadRenderer.shadowRoot) return;
const repliesRenderer = threadRenderer.shadowRoot.querySelector("#replies > bili-comment-replies-renderer");
if (!repliesRenderer || !repliesRenderer.shadowRoot) return;
const viewMoreButton = repliesRenderer.shadowRoot.querySelector("#view-more > bili-text-button");
if (!viewMoreButton || !viewMoreButton.shadowRoot) return;
const button = viewMoreButton.shadowRoot.querySelector("button");
if (!button) return;
if (!this.viewMoreButtons.has(button)) {
this.processViewMoreButton(viewMoreButton, button, threadRenderer);
newButtonsFound++;
}
});
// 只在有新按钮时输出日志
if (newButtonsFound > 0) {
Utils.log('info', `新处理了 ${newButtonsFound} 个"点击查看"按钮,总计: ${this.viewMoreButtons.size}`);
}
} catch (error) {
Utils.log('error', '扫描按钮时出错:', error);
}
}
processViewMoreButton(container, button, threadRenderer) {
this.viewMoreButtons.add(button);
button.setAttribute('data-waterfall-processed', 'true');
// 先尝试提取评论信息
const commentInfo = this.extractCommentInfo(container, threadRenderer);
if (!commentInfo) {
// 如果无法提取评论信息,创建一个基本的信息对象
const basicInfo = {
rootId: 'unknown',
oid: this.extractVideoId() || 'unknown',
replyCount: 0,
container,
commentElement: threadRenderer
};
this.addExpandButtonToStableLocation(commentInfo.commentElement, basicInfo);
Utils.log('info', '已添加评论展开按钮(无法提取完整信息)');
return;
}
this.addExpandButtonToStableLocation(commentInfo.commentElement, commentInfo);
Utils.log('info', `已添加评论展开按钮,评论ID: ${commentInfo.rootId}, 回复数: ${commentInfo.replyCount}`);
}
// 将评论展开按钮添加到稳定的位置(优化版)
addExpandButtonToStableLocation(threadRenderer, commentInfo) {
try {
// 检查是否已经存在评论展开按钮
if (this.findExpandButton(threadRenderer)) {
Utils.log('info', '评论展开按钮已存在,跳过添加');
return;
}
// 直接使用已验证有效的容器选择器
let targetContainer = threadRenderer.shadowRoot?.querySelector("#replies");
if (targetContainer) {
Utils.log('info', '使用主要容器: #replies');
} else {
// 降级方案:使用threadRenderer本身
targetContainer = threadRenderer;
Utils.log('warn', '未找到#replies容器,使用threadRenderer作为降级方案');
}
// 创建评论展开按钮
const expandBtn = this.createExpandButton(commentInfo);
// 创建一个包装容器,使按钮更稳定并居中
const buttonWrapper = document.createElement('div');
buttonWrapper.className = 'bili-comment-expand-wrapper';
buttonWrapper.style.cssText = `
display: flex;
justify-content: center;
align-items: center;
margin: 8px 0;
position: relative;
z-index: 1000;
width: 100%;
`;
buttonWrapper.appendChild(expandBtn);
// 将按钮添加到稳定位置
if (targetContainer.shadowRoot) {
// 如果目标容器有shadowRoot,添加到shadowRoot中
targetContainer.shadowRoot.appendChild(buttonWrapper);
Utils.log('info', `评论展开按钮已添加到稳定位置(shadowRoot): ${targetContainer.tagName || 'unknown'}`);
} else {
// 否则直接添加到容器中
targetContainer.appendChild(buttonWrapper);
Utils.log('info', `评论展开按钮已添加到稳定位置: ${targetContainer.tagName || 'unknown'}`);
}
} catch (error) {
Utils.log('error', '添加评论展开按钮到稳定位置失败:', error);
Utils.log('info', '尝试使用降级方案...');
// 降级到原始方法
this.addExpandButton(commentInfo.container || threadRenderer, commentInfo);
}
}
// 创建评论展开按钮(提取为独立方法)
createExpandButton(commentInfo) {
const expandBtn = document.createElement('button');
expandBtn.className = 'bili-comment-expand-btn';
expandBtn.style.cssText = `
padding: 8px 16px;
background: linear-gradient(135deg, #00a1d6, #0084b4);
color: #ffffff;
border: 1px solid rgba(0, 161, 214, 0.3);
border-radius: 6px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(0, 161, 214, 0.2);
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
position: relative;
overflow: hidden;
min-width: 120px;
`;
// 添加展开图标和文字 - 使用展开箭头符号
expandBtn.innerHTML = `
展开回复
`;
// 悬停效果 - 更丰富的动画
expandBtn.onmouseover = () => {
expandBtn.style.background = 'linear-gradient(135deg, #40a9ff, #1890ff)';
expandBtn.style.transform = 'translateY(-2px) scale(1.05)';
expandBtn.style.boxShadow = '0 6px 12px rgba(0, 161, 214, 0.4)';
expandBtn.style.borderColor = 'rgba(64, 169, 255, 0.6)';
};
expandBtn.onmouseout = () => {
expandBtn.style.background = 'linear-gradient(135deg, #00a1d6, #0084b4)';
expandBtn.style.transform = 'translateY(0) scale(1)';
expandBtn.style.boxShadow = '0 2px 6px rgba(0, 161, 214, 0.2)';
expandBtn.style.borderColor = 'rgba(0, 161, 214, 0.3)';
};
// 点击效果
expandBtn.onmousedown = () => {
expandBtn.style.transform = 'translateY(0) scale(0.95)';
};
expandBtn.onmouseup = () => {
expandBtn.style.transform = 'translateY(-2px) scale(1.05)';
};
// 绑定点击事件
expandBtn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
this.handleExpandClick(commentInfo);
};
return expandBtn;
}
// 原始的添加评论展开按钮方法(作为降级选项)
addExpandButton(container, commentInfo) {
// 检查是否已经添加过评论展开按钮
if (container.querySelector('.bili-comment-expand-btn')) {
return;
}
const expandBtn = this.createExpandButton(commentInfo);
container.appendChild(expandBtn);
}
handleExpandClick(commentInfo) {
// 调用主控制器的处理函数
if (this.onViewMoreClick) {
this.onViewMoreClick(commentInfo);
}
}
extractCommentInfo(container, threadRenderer) {
try {
// 从容器文本中提取回复数量
const containerText = container.textContent || '';
const replyCountMatch = containerText.match(/(\d+)\s*条回复/) || containerText.match(/共\s*(\d+)\s*条/) || containerText.match(/(\d+)\s*回复/);
const replyCount = replyCountMatch ? parseInt(replyCountMatch[1], 10) : 0;
// 更强力的评论ID提取
const rootId = this.extractCommentId(threadRenderer);
const oid = this.extractVideoId();
Utils.log('info', `提取到的信息: rootId=${rootId}, oid=${oid}, replyCount=${replyCount}`);
if (!rootId) {
Utils.log('warn', '无法提取评论ID');
return null;
}
if (!oid) {
Utils.log('warn', '无法提取视频ID');
return null;
}
return {
rootId,
oid,
replyCount,
container,
commentElement: threadRenderer
};
} catch (error) {
Utils.log('error', '提取评论信息失败', error);
return null;
}
}
extractCommentId(threadRenderer) {
// 方法1: 从__data对象获取(最新的B站结构)
if (threadRenderer.__data && threadRenderer.__data.rpid) {
const rpid = threadRenderer.__data.rpid.toString();
Utils.log('info', `方法1获取到rpid: ${rpid}`);
return rpid;
}
// 方法2: 从data属性获取
if (threadRenderer.data && threadRenderer.data.rpid) {
const rpid = threadRenderer.data.rpid.toString();
Utils.log('info', `方法2获取到rpid: ${rpid}`);
return rpid;
}
// 方法3: 从Shadow DOM中的commentRenderer获取
if (threadRenderer.shadowRoot) {
const commentRenderer = threadRenderer.shadowRoot.querySelector('bili-comment-renderer');
if (commentRenderer) {
// 从commentRenderer的__data获取
if (commentRenderer.__data && commentRenderer.__data.rpid) {
const rpid = commentRenderer.__data.rpid.toString();
Utils.log('info', `方法3获取到rpid: ${rpid}`);
return rpid;
}
// 从commentRenderer的data属性获取
if (commentRenderer.data && commentRenderer.data.rpid) {
const rpid = commentRenderer.data.rpid.toString();
Utils.log('info', `方法3.1获取到rpid: ${rpid}`);
return rpid;
}
// 从属性获取
const rpidAttr = commentRenderer.getAttribute('data-rpid') ||
commentRenderer.getAttribute('rpid');
if (rpidAttr) {
Utils.log('info', `方法3.2获取到rpid: ${rpidAttr}`);
return rpidAttr;
}
}
}
// 方法4: 传统方法 - 从属性获取
let rpid = threadRenderer.getAttribute('data-rpid') ||
threadRenderer.getAttribute('rpid') ||
threadRenderer.getAttribute('data-id');
if (rpid) {
Utils.log('info', `方法4获取到rpid: ${rpid}`);
return rpid;
}
// 方法5: 从dataset获取
const dataRpid = threadRenderer.dataset?.rpid;
if (dataRpid) {
Utils.log('info', `方法5获取到rpid: ${dataRpid}`);
return dataRpid;
}
Utils.log('warn', '所有方法都无法获取到rpid');
return null;
}
extractIdFromComponent(component) {
// 尝试从组件本身的属性获取
const possibleAttributes = ['data-rpid', 'rpid', 'data-id', 'comment-id'];
for (const attr of possibleAttributes) {
const value = component.getAttribute(attr);
if (value) {
return value;
}
}
// 尝试从Shadow DOM内部查找
if (component.shadowRoot) {
const shadowElements = component.shadowRoot.querySelectorAll('[data-rpid], [rpid], [data-id]');
for (const element of shadowElements) {
for (const attr of possibleAttributes) {
const value = element.getAttribute(attr);
if (value) {
return value;
}
}
}
}
// 尝试从子元素查找
const childElements = component.querySelectorAll('[data-rpid], [rpid], [data-id]');
for (const element of childElements) {
for (const attr of possibleAttributes) {
const value = element.getAttribute(attr);
if (value) {
return value;
}
}
}
// 如果还是找不到,尝试从URL或其他地方提取
try {
// 检查组件内是否有包含ID的链接
const links = component.querySelectorAll('a[href]');
for (const link of links) {
const href = link.getAttribute('href') || '';
const idMatch = href.match(/\/(\d+)/);
if (idMatch) {
return idMatch[1];
}
}
} catch (error) {
// 静默处理错误
}
return null;
}
extractVideoId() {
// 方法1: 从URL提取
const url = window.location.href;
const match = url.match(/\/video\/(?:av(\d+)|BV([a-zA-Z0-9]+))/);
if (match) {
if (match[1]) {
// av号直接返回
return match[1];
} else if (match[2]) {
// BV号需要转换,但先尝试从页面数据获取对应的aid
const aid = this.getAidFromPageData();
if (aid) {
return aid;
}
// 如果无法获取aid,返回BV号(某些API可能支持)
return match[2];
}
}
// 方法2: 从页面数据获取
const aid = this.getAidFromPageData();
if (aid) {
return aid;
}
// 方法3: 从meta标签获取
const metaAid = document.querySelector('meta[property="og:url"]');
if (metaAid) {
const metaMatch = metaAid.content.match(/\/video\/av(\d+)/);
if (metaMatch) {
return metaMatch[1];
}
}
return null;
}
getAidFromPageData() {
try {
// 尝试多种可能的全局变量
const sources = [
() => window.__INITIAL_STATE__?.videoData?.aid,
() => window.__initialState__?.videoData?.aid,
() => window.__INITIAL_STATE__?.aid,
() => window.__initialState__?.aid,
() => window.aid,
() => {
// 从页面中的script标签查找
const scripts = document.querySelectorAll('script');
for (const script of scripts) {
const content = script.textContent || '';
const aidMatch = content.match(/"aid":(\d+)/);
if (aidMatch) {
return aidMatch[1];
}
}
return null;
}
];
for (const source of sources) {
const aid = source();
if (aid) {
return aid.toString();
}
}
} catch (error) {
// 静默处理错误
}
return null;
}
setViewMoreClickHandler(handler) {
this.onViewMoreClick = handler;
}
getProcessedButtonCount() {
return this.viewMoreButtons.size;
}
destroy() {
if (this.observer) {
this.observer.disconnect();
this.observer = null;
}
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
this.isObserving = false;
this.viewMoreButtons.clear();
this.onViewMoreClick = null;
}
}
// 主控制器
class BilibiliCommentExpandController {
constructor() {
this.domWatcher = new DOMWatcher();
this.commentAPI = new BilibiliCommentAPI();
this.isInitialized = false;
}
async initialize() {
if (this.isInitialized) {
Utils.log('warn', '脚本已初始化');
return;
}
try {
Utils.log('info', '开始初始化Bilibili评论展开助手脚本');
this.setupEventHandlers();
this.domWatcher.observeCommentSection();
this.isInitialized = true;
Utils.log('info', 'Bilibili评论展开助手脚本初始化完成');
} catch (error) {
Utils.log('error', '脚本初始化失败', error);
throw error;
}
}
setupEventHandlers() {
this.domWatcher.setViewMoreClickHandler((commentInfo) => {
this.handleViewMoreClick(commentInfo);
});
Utils.log('info', '事件处理函数已设置');
}
async handleViewMoreClick(commentInfo) {
try {
Utils.log('info', '处理评论展开按钮点击', commentInfo);
// 显示加载提示
this.showLoadingIndicator();
// 尝试从按钮文本和周围元素中提取回复数量
const buttonText = commentInfo.container.textContent || '';
// 尝试多种模式匹配回复数量
let replyCount = 0;
const patterns = [
/(\d+)\s*条回复/,
/共\s*(\d+)\s*条/,
/(\d+)\s*回复/,
/(\d+)\s*replies?/i
];
for (const pattern of patterns) {
const match = buttonText.match(pattern);
if (match) {
replyCount = parseInt(match[1], 10);
break;
}
}
// 如果按钮文本中没有找到,尝试从父元素中查找
if (replyCount === 0) {
const parentText = commentInfo.commentElement?.textContent || '';
for (const pattern of patterns) {
const match = parentText.match(pattern);
if (match) {
replyCount = parseInt(match[1], 10);
break;
}
}
}
// 尝试从__data中获取回复数量
if (replyCount === 0 && commentInfo.commentElement?.__data?.rcount) {
replyCount = commentInfo.commentElement.__data.rcount;
Utils.log('info', `从__data获取到回复数量: ${replyCount}`);
}
// 获取真实的评论回复数据
let realReplies = [];
let apiError = null;
// 只要有评论ID和视频ID就尝试调用API,不依赖回复数量检测
if (commentInfo.rootId && commentInfo.oid && commentInfo.rootId !== 'unknown') {
try {
Utils.log('info', `开始获取回复数据: oid=${commentInfo.oid}, rootId=${commentInfo.rootId}, 预期回复数=${replyCount}`);
realReplies = await this.commentAPI.getAllReplies(commentInfo.oid, commentInfo.rootId);
Utils.log('info', `成功获取 ${realReplies.length} 条真实回复数据`);
// 如果API返回了数据,更新回复数量
if (realReplies.length > 0 && replyCount === 0) {
replyCount = realReplies.length;
Utils.log('info', `根据API结果更新回复数量: ${replyCount}`);
}
} catch (error) {
Utils.log('error', '获取真实回复数据失败:', error);
apiError = error;
}
} else {
Utils.log('warn', `跳过API调用: rootId=${commentInfo?.rootId}, oid=${commentInfo?.oid}, replyCount=${replyCount}`);
}
// 创建评论展开弹出框,传入真实数据
Utils.log('info', `创建弹出框: replyCount=${replyCount}, realReplies.length=${realReplies.length}, hasError=${!!apiError}`);
this.createExpandModal(replyCount, buttonText, commentInfo, realReplies, apiError);
// 隐藏加载提示
this.hideLoadingIndicator();
} catch (error) {
Utils.log('error', '处理"点击查看"按钮失败', error);
this.hideLoadingIndicator();
}
}
showLoadingIndicator() {
const loading = document.createElement('div');
loading.id = 'bili-comment-expand-loading';
loading.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 12px 20px;
border-radius: 4px;
z-index: 10001;
font-size: 14px;
`;
loading.textContent = '正在加载评论...';
document.body.appendChild(loading);
}
hideLoadingIndicator() {
const loading = document.getElementById('bili-comment-expand-loading');
if (loading) {
loading.remove();
}
}
createExpandModal(replyCount, buttonText, commentInfo, realReplies = [], apiError = null) {
// 创建遮罩层
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(4px);
`;
// 创建弹出框 - 暗色主题,模仿Bilibili原生设计
const modal = document.createElement('div');
modal.style.cssText = `
background: #1f1f1f;
border: 1px solid #3a3a3a;
border-radius: 8px;
width: 85%;
max-width: 900px;
max-height: 85%;
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
color: #e1e2e3;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
`;
// 创建头部 - 暗色主题
const header = document.createElement('div');
header.style.cssText = `
padding: 16px 20px;
border-bottom: 1px solid #3a3a3a;
display: flex;
justify-content: space-between;
align-items: center;
background: #2a2a2a;
border-radius: 8px 8px 0 0;
`;
const title = document.createElement('h3');
title.style.cssText = `
margin: 0;
font-size: 16px;
font-weight: 500;
color: #e1e2e3;
`;
title.textContent = `评论回复 (${replyCount}条)`;
const closeButton = document.createElement('button');
closeButton.style.cssText = `
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: #9499a0;
padding: 4px;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s ease;
`;
closeButton.textContent = '×';
closeButton.onclick = () => overlay.remove();
// 关闭按钮悬停效果
closeButton.onmouseover = () => {
closeButton.style.background = '#3a3a3a';
closeButton.style.color = '#ffffff';
};
closeButton.onmouseout = () => {
closeButton.style.background = 'none';
closeButton.style.color = '#9499a0';
};
header.appendChild(title);
header.appendChild(closeButton);
// 创建内容区域 - 暗色主题,左对齐
const body = document.createElement('div');
body.style.cssText = `
flex: 1;
overflow: auto;
background: #1f1f1f;
color: #9499a0;
text-align: left;
`;
// 根据是否有真实数据来显示不同内容
if (realReplies && realReplies.length > 0) {
// 显示真实的回复数据
this.renderRepliesContent(body, realReplies, replyCount);
} else if (replyCount > 0) {
// 显示加载失败或无数据的提示 - 暗色主题
const errorMsg = apiError ? apiError.message : '未知错误';
body.innerHTML = `
⚠️ 无法获取回复数据
检测到 ${replyCount} 条回复,但API请求失败
按钮文本: "${buttonText}"
错误信息: ${errorMsg}
评论ID: ${commentInfo?.rootId || '未获取到'}
视频ID: ${commentInfo?.oid || '未获取到'}
API URL: ${CONFIG.API_BASE}/x/v2/reply/reply?type=${CONFIG.COMMENT_TYPE}&oid=${commentInfo?.oid}&root=${commentInfo?.rootId}
可能原因:网络问题、API限制、需要登录或评论ID提取失败
✅ 基础架构已完成
暂无回复数据
按钮文本: "${buttonText}"
父元素文本:
${parentText.substring(0, 300)}...
容器HTML:
${containerHTML}...
脚本已成功工作!
✅ 成功找到并处理"点击查看"按钮
✅ 弹出框功能完全正常
✅ 基础架构已完成
✅ 用户名点击跳转功能
✅ 视频链接识别功能
✅ 时间排序正序/倒序切换
✅ 独立瀑布流按钮
Bilibili评论瀑布流 - 排序功能已优化