您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在 Stage1 论坛展开引用块,显示完整的被引用帖子内容。
// ==UserScript== // @name Stage1 Quote Expander with API Support // @name:zh-CN Stage1 论坛引用内容展开器 // @namespace user-NITOUCHE // @version 1.2.0 // @description Expands quote blocks on Stage1 forums to display full quoted post content. // @description:zh-CN 在 Stage1 论坛展开引用块,显示完整的被引用帖子内容。 // @author DS泥头车 // @match https://*.stage1st.com/2b/thread-* // @icon https://bbs.stage1st.com/favicon.ico // @grant GM_xmlhttpRequest // @grant GM_addStyle // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/jsrender.min.js // @license MIT // @run-at document-end // ==/UserScript== (function() { 'use strict'; const $ = jQuery.noConflict(); const api = 'https://app.saraba1st.com/2b/api/app'; const dialogTmpl = $.templates(` <div id="login-dialog" style="width: 400px; height: 260px; position: fixed; top: 50%; left: 50%; margin-left: -200px; margin-top: -130px; z-index: 999; background: #F6F7EB; border: 3px solid #CCCC99; padding-left: 20px;"> <div style="width: 100%; padding-top: 20px;">通过s1官方app接口查看不可见内容,需要单独登录</div> <div style="width: 100%; padding-top: 20px"><input type="text" id="username" value="{{:username}}" placeholder="用户名"></div> <div style="width: 100%; padding-top: 20px"><input type="password" id="password" value="{{:password}}" placeholder="密码"></div> <div style="width: 100%; padding-top: 20px"> <select id="questionId"> <option value="0">安全提问(未设置请忽略)</option> <option value="1">母亲的名字</option> <option value="2">爷爷的名字</option> <option value="3">父亲出生的城市</option> <option value="4">您其中一位老师的名字</option> <option value="5">您个人计算机的型号</option> <option value="6">您最喜欢的餐馆名称</option> <option value="7">驾驶执照最后四位数字</option> </select> </div> <div id="answer-row" hidden> <div style="width: 100%; padding-top: 20px"><input type="text" id="answer" placeholder="答案"></div> </div> <div style="width: 100%; padding-top: 20px"><button id="login-confirm">确定</button></div> <div style="width: 100%; padding-top: 20px; color: red">{{:msg}}</div> </div>`); // **Modified postTmpl: Simplified to render only message content** const postTmpl = $.templates(` {{:message}} {{if errorMessage}} <div style="color: var(--quote-error-color, red); font-size: smaller; margin-top: 5px;"> <b>加载失败:</b> {{:errorMessage}} </div> {{/if}} `); function login(username, password, questionId, answer) { const data = { username: username, password: password } if (questionId !== '0') { data.questionid = questionId; data.answer = answer; } $.ajax({ type: 'POST', url: api + '/user/login', data: data, success: function (resp) { const code = resp.code.toString(); if (code.startsWith('50')) { loginAndReplaceThreadContent({username, password, msg: resp.message}); return; } localStorage.setItem('app_sid', resp.data.sid); $('#login-dialog').remove(); // After successful login, re-process quotes to fetch content via API if needed processAllQuotes(); // Call function to re-process quotes }, error: function (err) { loginAndReplaceThreadContent({username, password, msg: '请求错误'}); } }); } function loginAndReplaceThreadContent(data) { $('#login-dialog').remove(); $('body').append(dialogTmpl.render(data)); const rawHeight = $('#login-dialog').height(); $('#questionId').change(function () { let questionId = $(this).val(); if (questionId === '0') { $('#login-dialog').height(rawHeight); $('#answer-row').hide(); } else { $('#login-dialog').height(rawHeight + 44); $('#answer-row').show(); } }); $('#login-confirm').click(function () { const username = $('#username').val(); const password = $('#password').val(); const questionId = $('#questionId').val(); const answer = $('#answer').val(); login(username, password, questionId, answer); }); } function handleRequest(resp, resolve, reject) { resp = typeof resp === 'string' ? JSON.parse(resp) : resp; const code = resp.code.toString(); if (code.startsWith('50')) { localStorage.removeItem('app_sid'); reject(); return; } resolve(resp.data); } let sid = localStorage.getItem('app_sid'); function fetchQuoteContentFromAPI(pid, blockquote, quoteHeaderHTML, ptid, originalQuoteContent) { if (!sid) { loginAndReplaceThreadContent({msg: "需要登录S1 App账号才能查看被禁言内容"}); renderBlockquoteWithError(blockquote, quoteHeaderHTML, originalQuoteContent, "需要登录S1 App账号"); return; } if (!ptid) { renderBlockquoteWithError(blockquote, quoteHeaderHTML, originalQuoteContent, "无法获取主题ID"); console.error("无法获取主题ID (ptid) - ptid was not passed to fetchQuoteContentFromAPI correctly."); return; } $.ajax({ type: 'POST', url: api + '/thread/page', data: { sid: sid, tid: ptid, pageNo: 1 }, success: function (resp) { const code = resp.code.toString(); if (code.startsWith('50')) { localStorage.removeItem('app_sid'); loginAndReplaceThreadContent({msg: resp.message}); renderBlockquoteWithError(blockquote, quoteHeaderHTML, originalQuoteContent, "API请求失败,请重新登录"); return; } const postList = resp.data.list; let foundPostData = null; for (const post of postList) { if (post.pid.toString() === pid) { foundPostData = post; break; } } console.log("API Response (resp):", resp); console.log("API Response Data (postList):", postList); console.log("Found Post Data for PID", pid, ":", foundPostData); if (foundPostData && foundPostData.message) { const renderedContent = postTmpl.render(foundPostData); blockquote.innerHTML = quoteHeaderHTML + '<br>' + renderedContent; // **Directly set innerHTML, combining header and rendered content** processAllQuotes(); } else { renderBlockquoteWithError(blockquote, quoteHeaderHTML, originalQuoteContent, "API内容为空或未找到PID"); console.warn("API returned empty content or PID not found for pid:", pid, "in thread page API response"); } }, error: function (err) { renderBlockquoteWithError(blockquote, quoteHeaderHTML, originalQuoteContent, "API请求出错"); console.error("API request error:", err); } }); } function renderBlockquoteWithError(blockquote, quoteHeaderHTML, originalQuoteContent, errorMessage) { let newBlockquoteHTML = ''; if (quoteHeaderHTML) { newBlockquoteHTML += quoteHeaderHTML + '<br>'; } // Render postTmpl with originalQuoteContent as message and errorMessage newBlockquoteHTML += postTmpl.render({ message: originalQuoteContent, errorMessage: errorMessage }); blockquote.innerHTML = newBlockquoteHTML; } function processQuoteDiv(quoteDiv) { const blockquote = quoteDiv.querySelector('blockquote'); if (!blockquote) return; const quoteText = blockquote.textContent.trim(); if (quoteText.endsWith(' ...')) { const linkElement = quoteDiv.querySelector('font[size="2"] a'); if (!linkElement) return; const postLink = linkElement.href; const urlParams = new URLSearchParams(new URL(postLink).search); const pid = urlParams.get('pid'); const ptid = urlParams.get('ptid'); console.log("Debug processQuoteDiv - ptid:", ptid, "pid:", pid, "postLink:", postLink); if (pid && ptid) { // 1. 提前提取引用头 HTML let quoteHeaderHTML = ''; let originalQuoteContent = ''; try { const headerElements = blockquote.querySelectorAll('font[size="2"]'); const tempBlockquote = blockquote.cloneNode(true); // Clone before header extraction headerElements.forEach(headerElement => { quoteHeaderHTML += headerElement.outerHTML + '<br>'; }); // Remove header elements from the cloned blockquote headerElements.forEach(headerElement => { if (headerElement.parentNode && headerElement.parentNode.nextSibling && headerElement.parentNode.nextSibling.nodeName === 'BR') { tempBlockquote.removeChild(headerElement.parentNode.nextSibling); // Remove <br> after <font> } tempBlockquote.removeChild(headerElement.parentNode); // Remove <font> parent }); originalQuoteContent = tempBlockquote.innerHTML.trim(); } catch (error) { console.warn("Error extracting quote header:", error); originalQuoteContent = blockquote.innerHTML.trim(); // Fallback to original content on error } GM_xmlhttpRequest({ url: postLink, method: 'GET', onload: function(response) { if (response.status === 200) { const parser = new DOMParser(); const doc = parser.parseFromString(response.responseText, 'text/html'); const postContentSelector = '#postmessage_' + pid; const originalPostContentElement = doc.querySelector(postContentSelector); if (originalPostContentElement) { let fullPostContentHTML = originalPostContentElement.innerHTML; // **Remove leading <br> and whitespace from fullPostContentHTML** fullPostContentHTML = fullPostContentHTML.replace(/^(\s*<br\s*\/?>\s*)+/, ''); blockquote.innerHTML = quoteHeaderHTML + '<br>' + fullPostContentHTML; // **Directly set innerHTML for success** processAllQuotes(); // **Re-process all quotes after successful expansion** } else { // If GM_xmlhttpRequest fails to find content, try API fetchQuoteContentFromAPI(pid, blockquote, quoteHeaderHTML, ptid, originalQuoteContent); } } else { // If GM_xmlhttpRequest fails (e.g., 404, 500), try API fetchQuoteContentFromAPI(pid, blockquote, quoteHeaderHTML, ptid, originalQuoteContent); } }, onerror: function(error) { // If GM_xmlhttpRequest errors out, try API fetchQuoteContentFromAPI(pid, blockquote, quoteHeaderHTML, ptid, originalQuoteContent); } }); } else { blockquote.innerHTML = '<span style="color: var(--quote-error-color, red);">无法获取帖子或主题ID</span>'; console.error("无法获取帖子或主题ID from link:", postLink); } } } function processAllQuotes() { const quoteDivs = document.querySelectorAll('div.quote'); quoteDivs.forEach(processQuoteDiv); } processAllQuotes(); // Initial processing of quotes on page load. Ask if this line still needed. GM_addStyle(` :root { --quote-loading-color: grey; --quote-error-color: red; } /* Optional: Copy CSS from "查看S1不可见内容" script here if you want login dialog styles */ #login-dialog { width: 400px; height: 260px; position: fixed; top: 50%; left: 50%; margin-left: -200px; margin-top: -130px; z-index: 999; background: #F6F7EB; border: 3px solid #CCCC99; padding-left: 20px; } #login-dialog div { width: 100%; padding-top: 20px; } #login-dialog input[type="text"], #login-dialog input[type="password"], #login-dialog select { width: calc(100% - 20px); padding: 8px; margin: 0; box-sizing: border-box; } #login-dialog button { padding: 8px 15px; cursor: pointer; } `); })();