Linux Do Summary

Add button to summarize and toggle content of the main post.

  1. // ==UserScript==
  2. // @name Linux Do Summary
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.0
  5. // @description Add button to summarize and toggle content of the main post.
  6. // @author Reno
  7. // @match https://linux.do/*
  8. // @grant GM_xmlhttpRequest
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. let state = {
  16. originalContent: '',
  17. toggled: false,
  18. apiTested: false
  19. };
  20.  
  21. function init() {
  22. observeDOMChanges();
  23. }
  24.  
  25. function addButtonAndEventListener() {
  26. const post = document.querySelector('#post_1');
  27. if (post && !document.getElementById('summaryToggleButton')) {
  28. const controlsContainer = document.querySelector('nav.post-controls');
  29. if (controlsContainer) {
  30. const button = document.createElement('button');
  31. button.id = 'summaryToggleButton';
  32. button.textContent = '总结';
  33. button.style.cssText = 'background-color: #4CAF50; color: white; padding: 5px 10px; border: none; border-radius: 5px; cursor: pointer;';
  34. controlsContainer.appendChild(button);
  35. button.addEventListener('click', handleButtonClick);
  36. state = {
  37. originalContent: '',
  38. toggled: false,
  39. apiTested: state.apiTested
  40. };
  41. }
  42. }
  43. }
  44.  
  45. async function handleButtonClick() {
  46. const summaryToggleButton = document.getElementById('summaryToggleButton');
  47. if (!state.toggled) {
  48. summaryToggleButton.disabled = true;
  49. summaryToggleButton.style.backgroundColor = '#808080';
  50.  
  51. let countdown = 10;
  52. summaryToggleButton.textContent = `${countdown} 秒`;
  53.  
  54. const countdownInterval = setInterval(() => {
  55. countdown--;
  56. summaryToggleButton.textContent = `${countdown} 秒`;
  57. if (countdown <= 0) {
  58. clearInterval(countdownInterval);
  59. summaryToggleButton.textContent = 'ಠ_ಠ';
  60. }
  61. }, 1000);
  62.  
  63. const configExists = await checkAndSetConfig();
  64. if (configExists) {
  65. try {
  66. const content = extractContent();
  67. const summary = await fetchSummary(content);
  68. if (summary) {
  69. displaySummary(formatSummary(summary));
  70. summaryToggleButton.textContent = '原文';
  71. summaryToggleButton.style.backgroundColor = '#007bff';
  72. state.toggled = true;
  73. window.scrollTo(0, 0);
  74.  
  75. }
  76. } catch (error) {
  77. console.error('处理内容时出错: ', error);
  78. summaryToggleButton.textContent = '重试';
  79. }
  80. } else {
  81. summaryToggleButton.textContent = '重试';
  82. }
  83.  
  84. clearInterval(countdownInterval);
  85. summaryToggleButton.disabled = false;
  86. } else {
  87. restoreOriginalContent();
  88. summaryToggleButton.textContent = '总结';
  89. summaryToggleButton.style.backgroundColor = '#4CAF50';
  90. state.toggled = false;
  91. window.scrollTo(0, 0);
  92.  
  93. }
  94. }
  95.  
  96. async function checkAndSetConfig() {
  97. const settings = { 'base_url': 'https://api.openai.com/v1/chat/completions', 'apikey': 'sk-k4NKZr82mTRTLCw984Da25536c374f4dB429A1Ee9dDa1087', 'model': 'gpt-4' };
  98. let configNeeded = false;
  99.  
  100. for (let key in settings) {
  101. let storedValue = localStorage.getItem(key);
  102. if (!storedValue) {
  103. storedValue = prompt(`请输入 ${key},示例:\n${settings[key]}`, '');
  104. if (storedValue) {
  105. localStorage.setItem(key, storedValue);
  106. } else {
  107. alert(`${key} 是必需的。没有 ${key},插件无法运行。`);
  108. return false;
  109. }
  110. configNeeded = true;
  111. }
  112. }
  113.  
  114. if (configNeeded && !state.apiTested) {
  115. try {
  116. await testAPIConnection();
  117. state.apiTested = true;
  118. } catch (error) {
  119. localStorage.removeItem('base_url');
  120. localStorage.removeItem('apikey');
  121. localStorage.removeItem('model');
  122. alert('API测试失败,请检查设置');
  123. return false;
  124. }
  125. }
  126. return true;
  127. }
  128.  
  129. async function testAPIConnection() {
  130. const response = await fetch(localStorage.getItem('base_url'), {
  131. headers: { 'Authorization': `Bearer ${localStorage.getItem('apikey')}` }
  132. });
  133.  
  134. if (!response.ok) throw new Error('API连接失败');
  135. }
  136.  
  137. async function processContent() {
  138. const content = extractContent();
  139. if (content) {
  140. try {
  141. const summary = await fetchSummary(content);
  142. const formattedSummary = formatSummary(summary);
  143. displaySummary(formattedSummary);
  144. } catch (error) {
  145. console.error('处理内容时出错: ', error);
  146. alert('处理错误,请重试');
  147. }
  148. }
  149. }
  150.  
  151. function extractContent() {
  152. let postStreamElement = document.querySelector('div.post-stream');
  153. if (postStreamElement && postStreamElement.querySelector('#post_1')) {
  154. let articleElement = postStreamElement.querySelector('#post_1');
  155. if (articleElement) {
  156. let cookedDiv = articleElement.querySelector('.cooked');
  157. if (cookedDiv) {
  158. let elementsData = [];
  159. let index = 0;
  160.  
  161. Array.from(cookedDiv.querySelectorAll('img, p, li')).forEach(node => {
  162. let tagName = node.tagName.toLowerCase() === 'li' ? 'p' : node.tagName.toLowerCase();
  163. let textContent = node.textContent.trim();
  164. let src = tagName === 'img' ? node.getAttribute('src')?.trim() : null;
  165.  
  166. if (tagName === 'p' && textContent.includes('\n')) {
  167. let contents = textContent.split(/\n+/).map(line => line.trim()).filter(line => line.length > 0);
  168. elementsData.push({ index, tagName, textContent: contents[0], src });
  169. index++;
  170. for (let i = 1; i < contents.length; i++) {
  171. elementsData.push({ index, tagName, textContent: contents[i], src });
  172. index++;
  173. }
  174. } else {
  175. elementsData.push({ index, tagName, textContent, src });
  176. index++;
  177. }
  178. });
  179.  
  180. let cleanedElementsData = elementsData.filter(({ tagName, textContent }) => tagName !== 'p' || textContent.length > 1);
  181. let uniqueElementsData = [];
  182. let uniqueTextContents = new Set();
  183. cleanedElementsData.forEach(({ tagName, textContent, src }) => {
  184. let contentKey = `${tagName}_${textContent}_${src}`;
  185. if (!uniqueTextContents.has(contentKey)) {
  186. uniqueElementsData.push({ tagName, textContent, src });
  187. uniqueTextContents.add(contentKey);
  188. }
  189. });
  190.  
  191. let htmlContent = "";
  192. uniqueElementsData.forEach(({ tagName, textContent, src }) => {
  193. if (tagName === 'p') {
  194. htmlContent += `<p>${textContent}</p>`;
  195. } else if (tagName === 'img') {
  196. htmlContent += `<img src="${src}" alt="${textContent}">`;
  197. }
  198. });
  199.  
  200. return htmlContent;
  201. }
  202. }
  203. }
  204. return '';
  205. }
  206.  
  207. async function fetchSummary(textContent) {
  208. const BASE_URL = window.localStorage.getItem('base_url');
  209. const API_KEY = window.localStorage.getItem('apikey');
  210. const MODEL = window.localStorage.getItem('model');
  211. const PROMPT = "以下是linux.do论坛的一个主题,帮我用中文简明扼要地梳理总结:";
  212. const headers = {
  213. "Content-Type": "application/json",
  214. "Authorization": `Bearer ${API_KEY}`
  215. };
  216. const body = JSON.stringify({ model: MODEL, messages: [{ role: "user", content: PROMPT + textContent }] });
  217.  
  218. try {
  219. const response = await fetch(BASE_URL, { method: "POST", headers, body });
  220. if (!response.ok) throw new Error(`API请求失败,状态码: ${response.status}`);
  221. const jsonResponse = await response.json();
  222. if (jsonResponse && jsonResponse.choices && jsonResponse.choices[0] && jsonResponse.choices[0].message) {
  223. return jsonResponse.choices[0].message.content;
  224. } else {
  225. throw new Error('API响应无效或格式不正确');
  226. }
  227. } catch (error) {
  228. console.error(error.message);
  229. alert("无法加载摘要,请稍后再试。错误详情: " + error.message);
  230. return null;
  231. }
  232. }
  233.  
  234. function formatSummary(text) {
  235. if (!text) return '无法加载摘要。';
  236. text = text.replace(/\n/g, '<br>').replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
  237. return text.replace(/^(#{1,6})\s(.*?)<br>/gm, (match, p1, p2) => `<h${p1.length}>${p2}</h${p1.length}><br>`)
  238. .replace(/- (.*?)<br>/g, '<li>$1</li><br>').replace(/<li>(.*?)<\/li><br><br>/g, '<ul><li>$1</li></ul><br>');
  239. }
  240.  
  241. function displaySummary(formattedSummary) {
  242. const contentElement = document.querySelector('#post_1 .cooked');
  243. if (contentElement) {
  244. state.originalContent = contentElement.innerHTML;
  245. contentElement.innerHTML = formattedSummary;
  246. document.getElementById('summaryToggleButton').textContent = '原文';
  247. state.toggled = true;
  248. }
  249. }
  250.  
  251. function restoreOriginalContent() {
  252. const contentElement = document.querySelector('#post_1 .cooked');
  253. if (contentElement && state.originalContent) {
  254. contentElement.innerHTML = state.originalContent;
  255. document.getElementById('summaryToggleButton').textContent = '总结';
  256. state.toggled = false;
  257. }
  258. }
  259.  
  260. function observeDOMChanges() {
  261. const observer = new MutationObserver((mutations) => {
  262. mutations.forEach((mutation) => {
  263. if (mutation.addedNodes.length) {
  264. addButtonAndEventListener();
  265. }
  266. });
  267. });
  268.  
  269. observer.observe(document.body, { childList: true, subtree: true });
  270. }
  271.  
  272. init();
  273. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址