WeiboPromptAssistant

Transform Weibo posts into AI prompts for analysis and processing

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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();
})();