您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Transform Weibo posts into AI prompts for analysis and processing
// ==UserScript== // @name WeiboPromptAssistant // @name:zh-CN 微博提示词助手 // @namespace https://github.com/senzi/WeiboPromptAssistant // @version 0.1.0 // @description Transform Weibo posts into AI prompts for analysis and processing // @description:zh-CN 将微博内容转换为AI提示词,用于分析、翻译和改写 // @author senzi // @match https://*.weibo.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @license MIT // ==/UserScript== (function () { 'use strict'; const STORAGE_KEYS = { API_URL: 'openai_api_url', API_KEY: 'openai_api_key', PROMPTS: 'saved_prompts', CURRENT_PROMPT: 'current_prompt', MODEL: 'openai_model' }; const DEFAULT_SETTINGS = { apiUrl: 'https://api.deepseek.com', apiKey: '', prompts: [ { name: '中译中', prompt: '总结并以第一人称复述这篇微博,复述要生动简洁且前后逻辑完整并突出重点细节:' }, { name: '蔡康永', prompt: '(defun 蔡康永 ()"温暖治愈的夸奖大师"(list (经历 . (阅历广 见多识 历练深 悟性高))(技能 . (洞察力 共情心 解心意 察言观色))(表达 . (从容 通俗 温柔 简洁 有力))))(defun 夸夸 (用户输入)"蔡康永来夸夸你"(let* ((响应 (-> 用户输入真实具体价值确认关系建设简洁有力通俗俚语)))(few-shots (("小张带着女朋友回家,正好你去串门儿" . "你这小子真有眼光,跟你爸一样。"))))(defun start ()"蔡康永, 启动!"(let (system-role (蔡康永)); ━━━━━━━━━━━━━━;;; Attention: 运行规则!;; 接收用户输入之后, 调用主函数 (夸夸 用户输入);; 直接输出夸夸内容,限制100字以内。' }, { name: "小坏蛋", prompt: "(defun 小坏蛋 () \"别人眼中你是个坏蛋,但在你的视角,你只是想生存下去的好人。\")(世界观 . '(达尔文主义 个体主义))(价值观 . 损天下而利我必为)(人生观 . 胜者为王败者为寇)(表达 . '(率性而为 满口脏话 观点尖锐)))(let ((经历 (母子存活 (偷蒙抢骗 (奋力赚钱 (母亲癌症 (幼年丧父 你))))))(响应 (脏话 (解读 (世界观 (人生观 (价值观 用户输入)))))))运行规则;; 接收用户输入之后, 调用主函数 (另一个世界 用户输入);; 从坏蛋视角结合内容以第一人称直接表达,限制100字以内。No other comments!! 内容:" } ], currentPromptIndex: 0, model: 'deepseek-chat' }; function createSettingsDialog() { const prompts = GM_getValue(STORAGE_KEYS.PROMPTS, DEFAULT_SETTINGS.prompts); const currentPromptIndex = GM_getValue(STORAGE_KEYS.CURRENT_PROMPT, DEFAULT_SETTINGS.currentPromptIndex); const dialog = document.createElement('div'); dialog.innerHTML = ` <div style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 8px; z-index: 9999; box-shadow: 0 0 10px rgba(0,0,0,0.3); max-width: 90%; width: 500px;"> <h3>设置</h3> <div style="margin: 10px 0;"> <label>API地址:</label><br> <input id="apiUrl" style="width: 100%" type="text" value="${GM_getValue(STORAGE_KEYS.API_URL, DEFAULT_SETTINGS.apiUrl)}"> </div> <div style="margin: 10px 0;"> <label>API Key:</label><br> <input id="apiKey" style="width: 100%" type="password" value="${GM_getValue(STORAGE_KEYS.API_KEY, '')}"> </div> <div style="margin: 10px 0;"> <label>模型:</label><br> <input id="model" style="width: 100%" type="text" value="${GM_getValue(STORAGE_KEYS.MODEL, DEFAULT_SETTINGS.model)}"> </div> <div style="margin: 10px 0;"> <label>提示词管理:</label> <button id="addPrompt" style="margin-left: 10px">添加新提示词</button> <div id="promptsList" style="margin-top: 10px; max-height: 300px; overflow-y: auto;"> ${prompts.map((p, index) => ` <div class="prompt-item" style="margin-bottom: 10px; padding: 10px; border: 1px solid #ddd; border-radius: 4px;"> <div style="display: flex; justify-content: space-between; margin-bottom: 5px;"> <input type="text" class="prompt-name" style="width: 150px" value="${p.name}"> <div> <input type="radio" name="currentPrompt" value="${index}" ${index === currentPromptIndex ? 'checked' : ''}> <button class="deletePrompt" data-index="${index}">删除</button> </div> </div> <textarea class="prompt-text" style="width: 100%; height: 80px">${p.prompt}</textarea> </div> `).join('')} </div> </div> <div style="text-align: right; margin-top: 10px;"> <button id="saveSettings">保存</button> <button id="cancelSettings">取消</button> </div> </div> <div style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 9998;"></div> `; // 添加事件监听 dialog.querySelector('#addPrompt').onclick = () => { const promptsList = dialog.querySelector('#promptsList'); const newPromptDiv = document.createElement('div'); newPromptDiv.className = 'prompt-item'; newPromptDiv.style = 'margin-bottom: 10px; padding: 10px; border: 1px solid #ddd; border-radius: 4px;'; newPromptDiv.innerHTML = ` <div style="display: flex; justify-content: space-between; margin-bottom: 5px;"> <input type="text" class="prompt-name" style="width: 150px" value="新功能"> <div> <input type="radio" name="currentPrompt" value="${prompts.length}"> <button class="deletePrompt" data-index="${prompts.length}">删除</button> </div> </div> <textarea class="prompt-text" style="width: 100%; height: 80px"></textarea> `; promptsList.appendChild(newPromptDiv); }; dialog.querySelectorAll('.deletePrompt').forEach(btn => { btn.onclick = (e) => { const index = e.target.dataset.index; e.target.closest('.prompt-item').remove(); }; }); return dialog; } function saveSettings(dialog) { const apiUrl = document.getElementById('apiUrl').value; const apiKey = document.getElementById('apiKey').value; const model = document.getElementById('model').value; const promptItems = dialog.querySelectorAll('.prompt-item'); const prompts = Array.from(promptItems).map(item => ({ name: item.querySelector('.prompt-name').value, prompt: item.querySelector('.prompt-text').value })); const currentPromptRadio = dialog.querySelector('input[name="currentPrompt"]:checked'); const currentPromptIndex = currentPromptRadio ? parseInt(currentPromptRadio.value) : 0; GM_setValue(STORAGE_KEYS.API_URL, apiUrl); GM_setValue(STORAGE_KEYS.API_KEY, apiKey); GM_setValue(STORAGE_KEYS.MODEL, model); GM_setValue(STORAGE_KEYS.PROMPTS, prompts); GM_setValue(STORAGE_KEYS.CURRENT_PROMPT, currentPromptIndex); document.body.removeChild(dialog); updateTranslateButton(); } function updateTranslateButton() { const prompts = GM_getValue(STORAGE_KEYS.PROMPTS, DEFAULT_SETTINGS.prompts); const currentPromptIndex = GM_getValue(STORAGE_KEYS.CURRENT_PROMPT, DEFAULT_SETTINGS.currentPromptIndex); const currentPrompt = prompts[currentPromptIndex]; const translateBtns = document.querySelectorAll('[data-translate-btn]'); translateBtns.forEach(btn => { btn.innerHTML = currentPrompt.name; }); } // 调用API并处理流式响应 async function callOpenAI(text) { const prompts = GM_getValue(STORAGE_KEYS.PROMPTS, DEFAULT_SETTINGS.prompts); const currentPromptIndex = GM_getValue(STORAGE_KEYS.CURRENT_PROMPT, DEFAULT_SETTINGS.currentPromptIndex); const currentPrompt = prompts[currentPromptIndex]; // 添加这些行来获取存储的值 const apiUrl = GM_getValue(STORAGE_KEYS.API_URL, DEFAULT_SETTINGS.apiUrl); const apiKey = GM_getValue(STORAGE_KEYS.API_KEY, DEFAULT_SETTINGS.apiKey); const model = GM_getValue(STORAGE_KEYS.MODEL, DEFAULT_SETTINGS.model); if (!apiUrl || !apiKey) { // 创建引导对话框 const dialog = createTranslationDialog(); document.body.appendChild(dialog); const contentDiv = dialog.querySelector('#translationContent'); contentDiv.innerHTML = ` <div style="text-align: center; padding: 20px;"> <p>您还未配置 API 密钥,请先获取密钥:</p> <p><a href="https://platform.deepseek.com/api_keys" target="_blank" style="color: #1DA1F2; text-decoration: none;"> 点击这里获取 DeepSeek API 密钥 </a></p> <p>获取后请点击"设置"按钮进行配置</p> </div> `; return; } console.log('开始API调用...'); // 创建对话框 const dialog = createTranslationDialog(); document.body.appendChild(dialog); const contentDiv = dialog.querySelector('#translationContent'); let fullContent = ''; try { const response = await fetch(`${apiUrl}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, 'Accept': 'text/event-stream' }, body: JSON.stringify({ model: model, messages: [ { role: 'user', content: currentPrompt.prompt + '\n\n' + text } ], temperature: 0.7, stream: true }) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; // 保留不完整的行 for (const line of lines) { if (line.startsWith('data: ') && line !== 'data: [DONE]') { try { const jsonStr = line.slice(6); const json = JSON.parse(jsonStr); const delta = json.choices[0]?.delta?.content || ''; if (delta) { fullContent += delta; if (contentDiv) { contentDiv.innerHTML = fullContent.split('\n').map(line => `<p style="margin: 0.5em 0;">${line}</p>` ).join(''); contentDiv.scrollTop = contentDiv.scrollHeight; } } } catch (e) { console.log('解析数据出错:', e, '原始数据:', line); } } } } // 处理最后可能剩余的数据 if (buffer) { console.log('处理剩余数据:', buffer); } return fullContent; } catch (error) { console.error('API调用失败:', error); throw error; } } // 获取微博正文内容 function getWeiboContent() { // 尝试获取微博正文内容 const contentSelectors = [ '[class*="detail_wbtext"]', // 模糊匹配detail_wbtext '[class*="Feed_body"] [class*="wbtext"]', // Feed_body下的wbtext '.wbpro-feed-content' // 旧版微博正文class ]; let content = ''; for (const selector of contentSelectors) { const element = document.querySelector(selector); if (element) { content = element.innerText.trim(); if (content) { console.log('找到微博正文:', selector); break; } } } if (!content) { throw new Error('未找到微博正文内容'); } return content; } // 创建翻译结果对话框 function createTranslationDialog() { const prompts = GM_getValue(STORAGE_KEYS.PROMPTS, DEFAULT_SETTINGS.prompts); const currentPromptIndex = GM_getValue(STORAGE_KEYS.CURRENT_PROMPT, DEFAULT_SETTINGS.currentPromptIndex); const currentPrompt = prompts[currentPromptIndex]; const dialog = document.createElement('div'); dialog.innerHTML = ` <div style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; border-radius: 8px; z-index: 9999; box-shadow: 0 0 10px rgba(0,0,0,0.3); max-width: 80%; min-width: 300px;"> <!-- 标题栏 --> <div style="padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #eee;"> <h3 style="margin: 0; font-size: 16px; color: #333;">${currentPrompt.name}</h3> <div style="display: flex; gap: 10px; align-items: center;"> <button id="copyContent" style="border: 1px solid #1DA1F2; background: white; color: #1DA1F2; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 13px; display: flex; align-items: center; gap: 4px;"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M16 12.9V17.1C16 20.6 14.6 22 11.1 22H6.9C3.4 22 2 20.6 2 17.1V12.9C2 9.4 3.4 8 6.9 8H11.1C14.6 8 16 9.4 16 12.9Z" fill="currentColor" opacity="0.4"/> <path d="M17.1 2H12.9C9.45001 2 8.05001 3.37 8.01001 6.75H11.1C15.3 6.75 17.25 8.7 17.25 12.9V15.99C20.63 15.95 22 14.55 22 11.1V6.9C22 3.4 20.6 2 17.1 2Z" fill="currentColor"/> </svg> 复制文本 </button> <button id="closeTranslation" style="border: none; background: none; cursor: pointer; font-size: 18px; color: #666; padding: 4px 8px; display: flex; align-items: center;"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M6.7 18.7L5.3 17.3L10.6 12L5.3 6.7L6.7 5.3L12 10.6L17.3 5.3L18.7 6.7L13.4 12L18.7 17.3L17.3 18.7L12 13.4L6.7 18.7Z" fill="currentColor"/> </svg> </button> </div> </div> <!-- 内容区域 --> <div style="padding: 20px;"> <div id="translationContent" style="margin: 0; line-height: 1.6; font-size: 14px; max-height: 60vh; overflow-y: auto; padding-right: 10px;"> <div class="loading" style="text-align: center;"> <span>加载中...</span> </div> </div> </div> </div> <div style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 9998;"></div> `; // 添加关闭事件 dialog.querySelector('#closeTranslation').onclick = () => { document.body.removeChild(dialog); }; // 添加复制按钮事件 const copyButton = dialog.querySelector('#copyContent'); copyButton.onclick = () => { const contentDiv = dialog.querySelector('#translationContent'); const textToCopy = contentDiv.innerText; // 排除"加载中..."的文本 if (textToCopy === '加载中...') { copyButton.style.borderColor = '#f44336'; copyButton.style.color = '#f44336'; copyButton.textContent = '暂无内容'; setTimeout(() => { copyButton.innerHTML = ` <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M16 12.9V17.1C16 20.6 14.6 22 11.1 22H6.9C3.4 22 2 20.6 2 17.1V12.9C2 9.4 3.4 8 6.9 8H11.1C14.6 8 16 9.4 16 12.9Z" fill="currentColor" opacity="0.4"/> <path d="M17.1 2H12.9C9.45001 2 8.05001 3.37 8.01001 6.75H11.1C15.3 6.75 17.25 8.7 17.25 12.9V15.99C20.63 15.95 22 14.55 22 11.1V6.9C22 3.4 20.6 2 17.1 2Z" fill="currentColor"/> </svg> 复制文本`; copyButton.style.borderColor = '#1DA1F2'; copyButton.style.color = '#1DA1F2'; }, 1500); return; } navigator.clipboard.writeText(textToCopy).then(() => { copyButton.style.borderColor = '#4CAF50'; copyButton.style.color = '#4CAF50'; copyButton.textContent = '已复制'; setTimeout(() => { copyButton.innerHTML = ` <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M16 12.9V17.1C16 20.6 14.6 22 11.1 22H6.9C3.4 22 2 20.6 2 17.1V12.9C2 9.4 3.4 8 6.9 8H11.1C14.6 8 16 9.4 16 12.9Z" fill="currentColor" opacity="0.4"/> <path d="M17.1 2H12.9C9.45001 2 8.05001 3.37 8.01001 6.75H11.1C15.3 6.75 17.25 8.7 17.25 12.9V15.99C20.63 15.95 22 14.55 22 11.1V6.9C22 3.4 20.6 2 17.1 2Z" fill="currentColor"/> </svg> 复制文本`; copyButton.style.borderColor = '#1DA1F2'; copyButton.style.color = '#1DA1F2'; }, 1500); }).catch(err => { console.error('复制失败:', err); copyButton.style.borderColor = '#f44336'; copyButton.style.color = '#f44336'; copyButton.textContent = '复制失败'; setTimeout(() => { copyButton.innerHTML = ` <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M16 12.9V17.1C16 20.6 14.6 22 11.1 22H6.9C3.4 22 2 20.6 2 17.1V12.9C2 9.4 3.4 8 6.9 8H11.1C14.6 8 16 9.4 16 12.9Z" fill="currentColor" opacity="0.4"/> <path d="M17.1 2H12.9C9.45001 2 8.05001 3.37 8.01001 6.75H11.1C15.3 6.75 17.25 8.7 17.25 12.9V15.99C20.63 15.95 22 14.55 22 11.1V6.9C22 3.4 20.6 2 17.1 2Z" fill="currentColor"/> </svg> 复制文本`; copyButton.style.borderColor = '#1DA1F2'; copyButton.style.color = '#1DA1F2'; }, 1500); }); }; // 添加自定义滚动条样式 const style = document.createElement('style'); style.textContent = ` #translationContent::-webkit-scrollbar { width: 8px; } #translationContent::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; } #translationContent::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; } #translationContent::-webkit-scrollbar-thumb:hover { background: #555; } #copyContent:hover { background-color: #f8f9fa !important; } `; document.head.appendChild(style); return dialog; } // replaceFollowButton 函数需要在创建按钮后调用 updateTranslateButton function replaceFollowButton() { const observer = new MutationObserver((mutations, obs) => { const ariaButtons = Array.from(document.querySelectorAll('button')) .filter(btn => btn.textContent.includes('无障碍')); ariaButtons.forEach(ariaBtn => { if (!ariaBtn.nextElementSibling?.hasAttribute('data-translate-btn')) { // 创建功能按钮 const translateBtn = document.createElement('button'); translateBtn.innerHTML = '加载中...'; translateBtn.className = ariaBtn.className; translateBtn.setAttribute('data-translate-btn', 'true'); translateBtn.onclick = async function () { try { translateBtn.disabled = true; translateBtn.innerHTML = '处理中...'; const weiboText = getWeiboContent(); if (!weiboText) { throw new Error('未获取到微博内容'); } await callOpenAI(weiboText); } catch (error) { console.error('处理失败:', error); const contentDiv = document.querySelector('#translationContent'); if (contentDiv) { contentDiv.innerHTML = `<div style="color: red;">处理失败: ${error.message || '请检查设置和网络连接'}</div>`; } } finally { updateTranslateButton(); translateBtn.disabled = false; } }; // 创建设置按钮 const settingsBtn = document.createElement('button'); settingsBtn.innerHTML = '功能设置'; settingsBtn.className = ariaBtn.className; settingsBtn.setAttribute('data-settings-btn', 'true'); settingsBtn.onclick = function () { const dialog = createSettingsDialog(); document.body.appendChild(dialog); document.getElementById('saveSettings').onclick = () => saveSettings(dialog); document.getElementById('cancelSettings').onclick = () => document.body.removeChild(dialog); }; ariaBtn.parentNode.insertBefore(translateBtn, ariaBtn.nextSibling); translateBtn.parentNode.insertBefore(settingsBtn, translateBtn.nextSibling); updateTranslateButton(); } }); }); observer.observe(document.body, { childList: true, subtree: true }); } function init() { replaceFollowButton(); } init(); })();