From 0aa45f7e27b0ecf29b50e9a6dff39da6fd083b6a Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 29 Jul 2026 00:15:41 +0800 Subject: [PATCH 1/2] fix: harden Zhihu enhancement observers --- Zhihu-Enhanced.user.js | 285 +++++++++++++++++++++++++++-------------- 1 file changed, 192 insertions(+), 93 deletions(-) diff --git a/Zhihu-Enhanced.user.js b/Zhihu-Enhanced.user.js index 222676ddb..85e7b6795 100644 --- a/Zhihu-Enhanced.user.js +++ b/Zhihu-Enhanced.user.js @@ -74,11 +74,11 @@ registerMenuCommand(); // 注册脚本菜单 function registerMenuCommand() { - if (menu_ID.length > menu_ALL.length){ // 如果菜单ID数组多于菜单数组,说明不是首次添加菜单,需要卸载所有脚本菜单 - for (let i=0;i { for (const mutation of mutations) { + if (mutation.target.nodeType !== Node.ELEMENT_NODE) continue; if (mutation.target.hasAttribute('script-collapsed')) return // 短的回答 if (mutation.target.classList.contains('RichContent')) { @@ -199,7 +200,7 @@ function getCollapsedAnswerObserver() { } // 长的回答 } else if (mutation.target.tagName === 'DIV' && !mutation.target.style.cssText && !mutation.target.className) { - if (mutation.target.parentElement.hasAttribute('script-collapsed')) return + if (mutation.target.parentElement?.hasAttribute('script-collapsed')) return //console.log('222',mutation.target, mutation.target.querySelector('.ContentItem-actions.Sticky [data-zop-retract-question]')) const button = mutation.target.querySelector('.ContentItem-actions.Sticky [data-zop-retract-question]'); if (button) { @@ -246,10 +247,12 @@ function defaultCollapsedAnswer() { function collapsedAnswer() { if (!menu_value('menu_collapsedAnswer')) return //console.log('1111', document.querySelector('.CornerAnimayedFlex')) - if (document.querySelector('.CornerAnimayedFlex') && !document.getElementById('collapsed-button')) { + const corner = document.querySelector('.CornerAnimayedFlex'); + const cornerButton = corner?.querySelector(':scope > button'); + if (corner && cornerButton && !document.getElementById('collapsed-button')) { // 向网页中插入收起全部回答按钮+样式+绑定点击事件 document.head.appendChild(document.createElement('style')).textContent = '.CornerButton{margin-bottom:8px !important;}.CornerButtons{bottom:45px !important;}'; - document.querySelector('.CornerAnimayedFlex').insertAdjacentHTML('afterBegin', ''); + corner.insertAdjacentHTML('afterBegin', ''); document.getElementById('collapsed-button').onclick = function () { // 收起所有评论(悬浮的 [收起评论]) @@ -293,9 +296,11 @@ function collapsedAnswer() { // 收起当前回答、评论(监听点击事件,点击网页两侧空白处) function collapsedNowAnswer(selectors) { + const root = document.querySelector(selectors); + if (!root) return; backToTop(selectors) // 快捷回到顶部 if (!menu_value('menu_collapsedNowAnswer')) return - document.querySelector(selectors).onclick = function(event){ + root.onclick = function(event){ if (event.target == this) { // 下面这段主要是 [收起回答],顺便 [收起评论](如果展开了的话) let rightButton = document.querySelector('.ContentItem-actions.Sticky.RichContent-actions.is-fixed.is-bottom') @@ -419,7 +424,9 @@ function collapsedNowAnswer(selectors) { // 回到顶部(监听点击事件,鼠标右键点击网页两侧空白处) function backToTop(selectors) { if (!menu_value('menu_backToTop')) return - document.querySelector(selectors).oncontextmenu = function(event){ + const root = document.querySelector(selectors); + if (!root) return; + root.oncontextmenu = function(event){ if (event.target == this) { event.preventDefault(); window.scrollTo(0,0) @@ -483,7 +490,7 @@ function blockLowCount(type) { const callback = (mutationsList, observer) => { for (const mutation of mutationsList) { for (const target of mutation.addedNodes) { - if (target.nodeType != 1) return + if (target.nodeType != 1) continue if (target.className === className2) { blockLowCount_1(target,menuUpvote,'upvote_num'); blockLowCount_1(target,menuComment,'comment_num'); @@ -500,7 +507,13 @@ function blockLowCount(type) { if (GM_getValue(menu)) { let item_ContentItem = item.querySelector('.ContentItem') if (item_ContentItem && item_ContentItem.dataset.zaExtraModule) { - let item2 = JSON.parse(item_ContentItem.dataset.zaExtraModule); + let item2; + try { + item2 = JSON.parse(item_ContentItem.dataset.zaExtraModule); + } catch (error) { + console.warn('解析信息流数据失败', error); + return; + } //console.log(item2) if (item2 && item2.card.content && Number(item2.card.content[type]) < Number(GM_getValue(menu))) { console.log('已屏蔽' + (type === 'upvote_num' ? '低赞':'低评') + (item_ContentItem.classList.contains('AnswerItem') ? '回答':'文章') + ':', item2.card.content[type] + '<' + GM_getValue(menu), item); @@ -564,8 +577,8 @@ function blockUsers(type) { let item = item1.querySelector('.ContentItem.AnswerItem, .ContentItem.ArticleItem'); // 用户名所在元素 if (item) { for (const keyword of menu_value('menu_customBlockUsers')) { // 遍历用户名黑名单 - if (keyword != '' && item.dataset.zop.indexOf('authorName":"' + keyword + '",') > -1) { // 找到就删除该信息流 - console.log('已屏蔽:' + item.dataset.zop); + if (keyword != '' && (item.dataset.zop || '').indexOf('authorName":"' + keyword + '",') > -1) { // 找到就删除该信息流 + console.log('已屏蔽:' + (item.dataset.zop || '')); item1.hidden = true; break; } @@ -583,13 +596,13 @@ function blockUsers(type) { const callback = (mutationsList, observer) => { for (const mutation of mutationsList) { for (const target of mutation.addedNodes) { - if (target.nodeType != 1) return + if (target.nodeType != 1) continue if (target.className === className2) { let item = target.querySelector('.ContentItem.AnswerItem, .ContentItem.ArticleItem'); // 用户名所在元素 if (item) { for (const keyword of menu_value('menu_customBlockUsers')) { // 遍历用户名黑名单 - if (keyword != '' && item.dataset.zop.indexOf('authorName":"' + keyword + '",') > -1) { // 找到就删除该信息流 - console.log('已屏蔽:' + item.dataset.zop); + if (keyword != '' && (item.dataset.zop || '').indexOf('authorName":"' + keyword + '",') > -1) { // 找到就删除该信息流 + console.log('已屏蔽:' + (item.dataset.zop || '')); target.hidden = true; break; } @@ -608,13 +621,13 @@ function blockUsers(type) { const blockUsers_question_ = (mutationsList, observer) => { for (const mutation of mutationsList) { for (const target of mutation.addedNodes) { - if (target.nodeType != 1) return + if (target.nodeType != 1) continue if (target.className === 'List-item' || target.className === 'Card AnswerCard') { let item1 = target.querySelector('.ContentItem.AnswerItem'); if (item1) { menu_value('menu_customBlockUsers').forEach(function(item2){ // 遍历用户黑名单 - if (item1.dataset.zop.indexOf('authorName":"' + item2 + '",') > -1) { // 找到就删除该回答 - console.log('已屏蔽:' + item1.dataset.zop) + if ((item1.dataset.zop || '').indexOf('authorName":"' + item2 + '",') > -1) { // 找到就删除该回答 + console.log('已屏蔽:' + (item1.dataset.zop || '')) target.hidden = true; } }) @@ -627,13 +640,13 @@ function blockUsers(type) { const blockUsers_question_answer_ = (mutationsList, observer) => { for (const mutation of mutationsList) { for (const target of mutation.addedNodes) { - if (target.nodeType != 1) return + if (target.nodeType != 1) continue target.querySelectorAll('.List-item, .Card.AnswerCard').forEach(function(item){ let item1 = item.querySelector('.ContentItem.AnswerItem'); if (item1) { menu_value('menu_customBlockUsers').forEach(function(item2){ // 遍历用户黑名单 - if (item1.dataset.zop.indexOf('authorName":"' + item2 + '",') > -1) { // 找到就删除该回答 - console.log('已屏蔽:' + item1.dataset.zop) + if ((item1.dataset.zop || '').indexOf('authorName":"' + item2 + '",') > -1) { // 找到就删除该回答 + console.log('已屏蔽:' + (item1.dataset.zop || '')) item.hidden = true; } }) @@ -656,8 +669,8 @@ function blockUsers(type) { let item1 = item.querySelector('.ContentItem.AnswerItem'); if (item1) { menu_value('menu_customBlockUsers').forEach(function(item2){ // 遍历用户黑名单 - if (item1.dataset.zop.indexOf('authorName":"' + item2 + '",') > -1) { // 找到就删除该回答 - console.log('已屏蔽:' + item1.dataset.zop) + if ((item1.dataset.zop || '').indexOf('authorName":"' + item2 + '",') > -1) { // 找到就删除该回答 + console.log('已屏蔽:' + (item1.dataset.zop || '')) item.hidden = true; } }) @@ -691,7 +704,7 @@ function blockUsers(type) { if (location.search.indexOf('type=content') === -1) return // 目前只支持搜索页的 [综合] for (const mutation of mutationsList) { for (const target of mutation.addedNodes) { - if (target.nodeType != 1) return + if (target.nodeType != 1) continue let item = target.querySelector('.Card.SearchResult-Card[data-za-detail-view-path-module="AnswerItem"] .RichText.ztext.CopyrightRichText-richText b, .Card.SearchResult-Card[data-za-detail-view-path-module="PostItem"] .RichText.ztext.CopyrightRichText-richText b'); if (item) { for (const keyword of menu_value('menu_customBlockUsers')) { // 遍历用户名黑名单 @@ -713,7 +726,7 @@ function blockUsers(type) { const callback = (mutationsList, observer) => { for (const mutation of mutationsList) { for (const target of mutation.addedNodes) { - if (target.nodeType != 1) return + if (target.nodeType != 1) continue //console.log(target) if (target.tagName == 'DIV' && target.className.indexOf('css-') == 0 && target.dataset.id == undefined) { let item = target.querySelector('a[href^="https://www.zhihu.com/people/"]>img.Avatar[alt][loading]') @@ -751,7 +764,7 @@ function blockUsers(type) { const callback = (mutationsList, observer) => { for (const mutation of mutationsList) { for (const target of mutation.addedNodes) { - if (target.nodeType != 1) return + if (target.nodeType != 1) continue //console.log(target, target.className) if (target.tagName == 'DIV' && target.className && (target.className.indexOf('css-') == 0 || target.style == 'opacity: 1;')) { const item = target.querySelector('.MemberButtonGroup.ProfileButtonGroup.HoverCard-buttons'), @@ -759,7 +772,7 @@ function blockUsers(type) { if (item1) { const name = item1.textContent, userid = item1.href.split('/')[4], users = menu_value('menu_customBlockUsers'); for (let num = 0;num