申请微博中译中

将微博内容翻译成更易理解的中文 Github:https://github.com/SomiaWhiteRing/chinese2chinese4weibo

  1. // ==UserScript==
  2. // @name 申请微博中译中
  3. // @namespace https://github.com/SomiaWhiteRing/chinese2chinese4weibo
  4. // @version 0.2
  5. // @description 将微博内容翻译成更易理解的中文 Github:https://github.com/SomiaWhiteRing/chinese2chinese4weibo
  6. // @author WhiteRing
  7. // @match https://*.weibo.com/*
  8. // @grant GM_setValue
  9. // @grant GM_getValue
  10. // @grant GM_xmlhttpRequest
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. // 存储设置的key
  18. const STORAGE_KEYS = {
  19. API_URL: 'openai_api_url',
  20. API_KEY: 'openai_api_key',
  21. PROMPT: 'default_prompt',
  22. MODEL: 'openai_model'
  23. };
  24.  
  25. // 默认设置
  26. const DEFAULT_SETTINGS = {
  27. apiUrl: 'https://api.deepseek.com',
  28. apiKey: '',
  29. prompt: '总结并以第一人称复述这篇微博,复述要生动简洁且前后逻辑完整并突出重点细节:',
  30. model: 'deepseek-chat'
  31. };
  32.  
  33. // 创建设置弹窗
  34. function createSettingsDialog() {
  35. const dialog = document.createElement('div');
  36. dialog.innerHTML = `
  37. <div style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  38. background: white; padding: 20px; border-radius: 8px; z-index: 9999;
  39. box-shadow: 0 0 10px rgba(0,0,0,0.3);">
  40. <h3>设置</h3>
  41. <div style="margin: 10px 0;">
  42. <label>API地址:</label><br>
  43. <input id="apiUrl" style="width: 300px" type="text" value="${GM_getValue(STORAGE_KEYS.API_URL, DEFAULT_SETTINGS.apiUrl)}">
  44. </div>
  45. <div style="margin: 10px 0;">
  46. <label>API Key:</label><br>
  47. <input id="apiKey" style="width: 300px" type="password" value="${GM_getValue(STORAGE_KEYS.API_KEY, '')}">
  48. </div>
  49. <div style="margin: 10px 0;">
  50. <label>模型:</label><br>
  51. <input id="model" style="width: 300px" type="text" value="${GM_getValue(STORAGE_KEYS.MODEL, DEFAULT_SETTINGS.model)}">
  52. </div>
  53. <div style="margin: 10px 0;">
  54. <label>默认提示词:</label><br>
  55. <textarea id="prompt" style="width: 300px; height: 100px">${GM_getValue(STORAGE_KEYS.PROMPT, DEFAULT_SETTINGS.prompt)}</textarea>
  56. </div>
  57. <div style="text-align: right; margin-top: 10px;">
  58. <button id="saveSettings">保存</button>
  59. <button id="cancelSettings">取消</button>
  60. </div>
  61. </div>
  62. <div style="position: fixed; top: 0; left: 0; right: 0; bottom: 0;
  63. background: rgba(0,0,0,0.5); z-index: 9998;"></div>
  64. `;
  65.  
  66. return dialog;
  67. }
  68.  
  69. // 保存设置
  70. function saveSettings(dialog) {
  71. const apiUrl = document.getElementById('apiUrl').value;
  72. const apiKey = document.getElementById('apiKey').value;
  73. const prompt = document.getElementById('prompt').value;
  74. const model = document.getElementById('model').value;
  75.  
  76. GM_setValue(STORAGE_KEYS.API_URL, apiUrl);
  77. GM_setValue(STORAGE_KEYS.API_KEY, apiKey);
  78. GM_setValue(STORAGE_KEYS.PROMPT, prompt);
  79. GM_setValue(STORAGE_KEYS.MODEL, model);
  80.  
  81. document.body.removeChild(dialog);
  82. }
  83.  
  84. // 调用API并处理流式响应
  85. async function callOpenAI(text) {
  86. const apiUrl = GM_getValue(STORAGE_KEYS.API_URL);
  87. const apiKey = GM_getValue(STORAGE_KEYS.API_KEY);
  88.  
  89. if (!apiUrl || !apiKey) {
  90. // 创建引导对话框
  91. const dialog = createTranslationDialog();
  92. document.body.appendChild(dialog);
  93. const contentDiv = dialog.querySelector('#translationContent');
  94. contentDiv.innerHTML = `
  95. <div style="text-align: center; padding: 20px;">
  96. <p>您还未配置 API 密钥,请先获取密钥:</p>
  97. <p><a href="https://platform.deepseek.com/api_keys"
  98. target="_blank"
  99. style="color: #1DA1F2; text-decoration: none;">
  100. 点击这里获取 DeepSeek API 密钥
  101. </a></p>
  102. <p>获取后请点击"翻译设置"按钮进行配置</p>
  103. </div>
  104. `;
  105. return;
  106. }
  107.  
  108. const prompt = GM_getValue(STORAGE_KEYS.PROMPT);
  109. const model = GM_getValue(STORAGE_KEYS.MODEL, DEFAULT_SETTINGS.model);
  110.  
  111. console.log('开始API调用...');
  112.  
  113. // 创建对话框
  114. const dialog = createTranslationDialog();
  115. document.body.appendChild(dialog);
  116. const contentDiv = dialog.querySelector('#translationContent');
  117. let fullContent = '';
  118.  
  119. try {
  120. const response = await fetch(`${apiUrl}/chat/completions`, {
  121. method: 'POST',
  122. headers: {
  123. 'Content-Type': 'application/json',
  124. 'Authorization': `Bearer ${apiKey}`,
  125. 'Accept': 'text/event-stream'
  126. },
  127. body: JSON.stringify({
  128. model: model,
  129. messages: [
  130. { role: 'user', content: prompt + '\n\n' + text }
  131. ],
  132. temperature: 0.7,
  133. stream: true
  134. })
  135. });
  136.  
  137. if (!response.ok) {
  138. throw new Error(`HTTP error! status: ${response.status}`);
  139. }
  140.  
  141. const reader = response.body.getReader();
  142. const decoder = new TextDecoder();
  143. let buffer = '';
  144.  
  145. while (true) {
  146. const { value, done } = await reader.read();
  147. if (done) break;
  148.  
  149. buffer += decoder.decode(value, { stream: true });
  150. const lines = buffer.split('\n');
  151. buffer = lines.pop() || ''; // 保留不完整的行
  152.  
  153. for (const line of lines) {
  154. if (line.startsWith('data: ') && line !== 'data: [DONE]') {
  155. try {
  156. const jsonStr = line.slice(6);
  157. const json = JSON.parse(jsonStr);
  158. const delta = json.choices[0]?.delta?.content || '';
  159.  
  160. if (delta) {
  161. fullContent += delta;
  162. if (contentDiv) {
  163. contentDiv.innerHTML = fullContent.split('\n').map(line =>
  164. `<p style="margin: 0.5em 0;">${line}</p>`
  165. ).join('');
  166. contentDiv.scrollTop = contentDiv.scrollHeight;
  167. }
  168. }
  169. } catch (e) {
  170. console.log('解析数据出错:', e, '原始数据:', line);
  171. }
  172. }
  173. }
  174. }
  175.  
  176. // 处理最后可能剩余的数据
  177. if (buffer) {
  178. console.log('处理剩余数据:', buffer);
  179. }
  180.  
  181. return fullContent;
  182.  
  183. } catch (error) {
  184. console.error('API调用失败:', error);
  185. throw error;
  186. }
  187. }
  188.  
  189. // 获取微博正文内容
  190. function getWeiboContent() {
  191. // 尝试获取微博正文内容
  192. const contentSelectors = [
  193. '[class*="detail_wbtext"]', // 模糊匹配detail_wbtext
  194. '[class*="Feed_body"] [class*="wbtext"]', // Feed_body下的wbtext
  195. '.wbpro-feed-content' // 旧版微博正文class
  196. ];
  197.  
  198. let content = '';
  199. for (const selector of contentSelectors) {
  200. const element = document.querySelector(selector);
  201. if (element) {
  202. content = element.innerText.trim();
  203. if (content) {
  204. console.log('找到微博正文:', selector);
  205. break;
  206. }
  207. }
  208. }
  209.  
  210. if (!content) {
  211. throw new Error('未找到微博正文内容');
  212. }
  213.  
  214. return content;
  215. }
  216.  
  217. // 创建翻译结果对话框
  218. function createTranslationDialog() {
  219. const dialog = document.createElement('div');
  220. dialog.innerHTML = `
  221. <div style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  222. background: white; border-radius: 8px; z-index: 9999;
  223. box-shadow: 0 0 10px rgba(0,0,0,0.3); max-width: 80%; min-width: 300px;">
  224. <!-- 标题栏 -->
  225. <div style="padding: 16px 20px; display: flex; justify-content: space-between;
  226. align-items: center; border-bottom: 1px solid #eee;">
  227. <h3 style="margin: 0; font-size: 16px; color: #333;">申请中译中!</h3>
  228. <button id="closeTranslation" style="border: none; background: none; cursor: pointer;
  229. font-size: 18px; color: #666; padding: 4px 8px;">✕</button>
  230. </div>
  231. <!-- 内容区域 -->
  232. <div style="padding: 20px;">
  233. <div id="translationContent" style="margin: 0; line-height: 1.6; font-size: 14px;
  234. max-height: 60vh; overflow-y: auto; padding-right: 10px;">
  235. <div class="loading" style="text-align: center;">
  236. <span>加载中...</span>
  237. </div>
  238. </div>
  239. </div>
  240. </div>
  241. <div style="position: fixed; top: 0; left: 0; right: 0; bottom: 0;
  242. background: rgba(0,0,0,0.5); z-index: 9998;"></div>
  243. `;
  244.  
  245. // 添加关闭事件
  246. dialog.querySelector('#closeTranslation').onclick = () => {
  247. document.body.removeChild(dialog);
  248. };
  249.  
  250. // 添加自定义滚动条样式
  251. const style = document.createElement('style');
  252. style.textContent = `
  253. #translationContent::-webkit-scrollbar {
  254. width: 8px;
  255. }
  256. #translationContent::-webkit-scrollbar-track {
  257. background: #f1f1f1;
  258. border-radius: 4px;
  259. }
  260. #translationContent::-webkit-scrollbar-thumb {
  261. background: #888;
  262. border-radius: 4px;
  263. }
  264. #translationContent::-webkit-scrollbar-thumb:hover {
  265. background: #555;
  266. }
  267. `;
  268. document.head.appendChild(style);
  269.  
  270. return dialog;
  271. }
  272.  
  273. // 替换关注按钮
  274. function replaceFollowButton() {
  275. const observer = new MutationObserver((mutations, obs) => {
  276. const ariaButtons = Array.from(document.querySelectorAll('button'))
  277. .filter(btn => btn.textContent.includes('无障碍'));
  278.  
  279. ariaButtons.forEach(ariaBtn => {
  280. if (!ariaBtn.nextElementSibling?.hasAttribute('data-translate-btn')) {
  281. // 创建翻译按钮
  282. const translateBtn = document.createElement('button');
  283. translateBtn.innerHTML = '中译中';
  284. translateBtn.className = ariaBtn.className;
  285. translateBtn.setAttribute('data-translate-btn', 'true');
  286. translateBtn.onclick = async function () {
  287. try {
  288. console.log('点击翻译按钮');
  289. translateBtn.disabled = true;
  290. translateBtn.innerHTML = '翻译中...';
  291.  
  292. const weiboText = getWeiboContent();
  293. console.log('获取到微博文本:', weiboText);
  294.  
  295. if (!weiboText) {
  296. throw new Error('未获取到微博内容');
  297. }
  298.  
  299. console.log('开始调用API');
  300. await callOpenAI(weiboText);
  301.  
  302. } catch (error) {
  303. console.error('翻译失败:', error);
  304. const contentDiv = document.querySelector('#translationContent');
  305. if (contentDiv) {
  306. contentDiv.innerHTML = `<div style="color: red;">翻译失败: ${error.message || '请检查设置和网络连接'}</div>`;
  307. }
  308. } finally {
  309. translateBtn.disabled = false;
  310. translateBtn.innerHTML = '中译中';
  311. }
  312. };
  313.  
  314. // 创建设置按钮
  315. const settingsBtn = document.createElement('button');
  316. settingsBtn.innerHTML = '翻译设置';
  317. settingsBtn.className = ariaBtn.className;
  318. settingsBtn.setAttribute('data-settings-btn', 'true');
  319. settingsBtn.onclick = function () {
  320. const dialog = createSettingsDialog();
  321. document.body.appendChild(dialog);
  322.  
  323. document.getElementById('saveSettings').onclick = () => saveSettings(dialog);
  324. document.getElementById('cancelSettings').onclick = () => document.body.removeChild(dialog);
  325. };
  326.  
  327. // 插入按钮
  328. ariaBtn.parentNode.insertBefore(translateBtn, ariaBtn.nextSibling);
  329. translateBtn.parentNode.insertBefore(settingsBtn, translateBtn.nextSibling);
  330. }
  331. });
  332. });
  333.  
  334. observer.observe(document.body, {
  335. childList: true,
  336. subtree: true
  337. });
  338. }
  339.  
  340. // 初始化
  341. function init() {
  342. replaceFollowButton();
  343. }
  344.  
  345. init();
  346. })();
  347.  

QingJ © 2025

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