Linux Do Translate

对回复进行翻译

目前為 2024-11-01 提交的版本,檢視 最新版本

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Linux Do Translate
// @namespace    linux-do-translate
// @version      0.1.5
// @author       delph1s
// @license      MIT
// @description  对回复进行翻译
// @match        https://linux.do/t/topic/*
// @connect      *
// @icon         https://cdn.linux.do/uploads/default/original/3X/9/d/9dd49731091ce8656e94433a26a3ef36062b3994.png
// @grant        unsafeWindow
// @grant        window.close
// @grant        window.focus
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/index.min.js
// @run-at       document-end
// ==/UserScript==

(function () {
  'use strict';
  const REQUIRED_CHARS = 6;
  const SPACE_PRESS_COUNT = 3; // 连按次数
  const SPACE_PRESS_TIMEOUT = 1500; // 连续按键的最大时间间隔(毫秒)
  const DEFAULT_CONFIG = {
    maxRetryTimes: 5,
    deeplxUrl: '',
    deeplxAuthKey: '',
    authKeyPrefix: {
      deeplx: '',
      deeplOfficial: 'deepl-auth-key:',
      openAIOfficial: 'oai-at:',
      openAIProxy: 'oai-at-proxy:',
    },
    enableTranslate: false,
    translateTargetLang: {
      deeplx: 'EN',
      deeplOfficial: 'EN',
      openAIOfficial: 'English',
      openAIProxy: 'English',
    },
    translateModel: 'gpt-3.5-turbo',
    translateLayout: 'up',
    translateSize: 150,
    enablePinYin: false,
    closeConfigAfterSave: true,
  };

  const uiIDs = {
    replyControl: 'reply-control',
    configButton: 'multi-lang-say-config-button',
    configPanel: 'multi-lang-say-config-panel',
    deeplxUrlInput: 'deeplx-url-input',
    deeplxAuthKeyInput: 'deeplx-authkey-input',
    translateModelInput: 'translate-model-input',
    translateSizeInput: 'translate-size-input',
  };

  let config = {
    maxRetryTimes: GM_getValue('maxRetryTimes', DEFAULT_CONFIG.maxRetryTimes),
    deeplxUrl: GM_getValue('deeplxUrl', DEFAULT_CONFIG.deeplxUrl),
    deeplxAuthKey: GM_getValue('deeplxAuthKey', DEFAULT_CONFIG.deeplxAuthKey),
    authKeyPrefix: GM_getValue('authKeyPrefix', DEFAULT_CONFIG.authKeyPrefix),
    enableTranslate: GM_getValue('enableTranslate', DEFAULT_CONFIG.enableTranslate),
    translateTargetLang: GM_getValue('translateTargetLang', DEFAULT_CONFIG.translateTargetLang),
    translateModel: GM_getValue('translateModel', DEFAULT_CONFIG.translateModel),
    translateLayout: GM_getValue('translateLayout', DEFAULT_CONFIG.translateLayout),
    translateSize: GM_getValue('translateSize', DEFAULT_CONFIG.translateSize),
    enablePinYin: GM_getValue('enablePinYin', DEFAULT_CONFIG.enablePinYin),
    closeConfigAfterSave: GM_getValue('closeConfigAfterSave', DEFAULT_CONFIG.closeConfigAfterSave),
  };

  const checkPinYinRequire = () => {
    // 检查是否加载了 pinyin 库
    if (typeof pinyinPro === 'undefined') {
      console.error('pinyin 库未正确加载!');
      config.enablePinYin = false;
      GM_setValue('enablePinYin', false);
      return false;
    }
    return true;
  };

  const genFormatDateTime = d => {
    return d.toLocaleString('zh-CN', {
      year: 'numeric',
      month: '2-digit',
      day: '2-digit',
      hour: '2-digit',
      minute: '2-digit',
      second: '2-digit',
      hour12: false,
    });
  };

  const genFormatNow = () => {
    return genFormatDateTime(new Date());
  };

  /**
   * 获取随机整数
   *
   * @param {number} start 范围开始
   * @param {number} end 范围结束
   * @returns
   */
  const randInt = (start, end) => {
    return Math.floor(Math.random() * (end - start + 1)) + start;
  };

  /**
   * 随机睡眠(毫秒)
   *
   * @param {number} start 范围开始
   * @param {number} end 范围结束
   */
  const randSleep = async (start = 2000, end = 3000) => {
    // 生成随机整数 randSleepTime,范围在 start 到 end 之间
    const randSleepTime = getRandomInt(start, end);
    // 睡眠时间
    return await new Promise(resolve => setTimeout(resolve, randSleepTime));
  };

  /**
   * 是否相同
   *
   * @param a
   * @param b
   * @returns
   */
  const isEqual = (a, b) => {
    if (a === null || a === undefined || b === null || b === undefined) {
      return a === b;
    }

    if (typeof a !== typeof b) {
      return false;
    }

    if (typeof a === 'string' || typeof a === 'number' || typeof a === 'boolean') {
      return a === b;
    }

    if (Array.isArray(a) && Array.isArray(b)) {
      if (a.length !== b.length) {
        return false;
      }

      return a.every((item, index) => isEqual(item, b[index]));
    }

    if (typeof a === 'object' && typeof b === 'object') {
      const keysA = Object.keys(a || {});
      const keysB = Object.keys(b || {});

      if (keysA.length !== keysB.length) {
        return false;
      }

      return keysA.every(key => isEqual(a[key], b[key]));
    }

    return false;
  };

  /**
   * 判断字符串中是否包含中文字符
   * @param {string} text
   * @returns {boolean}
   */
  const containsChinese = text => {
    return /[\u4e00-\u9fa5]/.test(text);
  };

  /**
   * 将中文字符串转换为带拼音的ruby标签字符串
   * @param {string} text
   * @returns {string}
   */
  const convertToRuby = text => {
    // 使用tiny-pinyin将中文转换为拼音,拼音之间用空格分隔
    const pinyin = pinyinPro.pinyin(text);
    // 构建ruby标签
    return `<ruby><rb>${text}</rb><rt>${pinyin}</rt></ruby>`;
  };

  const getInvertColor = hex => {
    // 去掉前面的“#”字符
    hex = hex.replace('#', '');

    // 如果输入的是3位的hex值,转换为6位的
    if (hex.length === 3) {
      hex = hex
        .split('')
        .map(c => c + c)
        .join('');
    }

    // 计算相反的颜色
    const r = (255 - parseInt(hex.slice(0, 2), 16)).toString(16).padStart(2, '0');
    const g = (255 - parseInt(hex.slice(2, 4), 16)).toString(16).padStart(2, '0');
    const b = (255 - parseInt(hex.slice(4, 6), 16)).toString(16).padStart(2, '0');

    return `#${r}${g}${b}`;
  };

  const deeplxReq = text => {
    return {
      url: config.deeplxUrl,
      headers: {
        'Content-Type': 'application/json',
      },
      data: JSON.stringify({
        text: text,
        target_lang: 'EN',
        source_lang: 'auto',
      }),
      responseType: 'json',
    };
  };

  const deeplxLinuxdoReq = text => {
    return {
      url: `https://api.deeplx.org/${config.deeplxAuthKey}/translate`,
      headers: {
        'Content-Type': 'application/json',
      },
      data: JSON.stringify({
        text: text,
        target_lang: 'EN',
        source_lang: 'auto',
      }),
      responseType: 'json',
    };
  };

  const deeplOfficialReq = text => {
    const authKey = config.deeplxAuthKey.replace(config.authKeyPrefix.deeplOfficial, '');
    const params = new URLSearchParams();
    params.append('text', text);
    params.append('target_lang', 'EN');
    params.append('source_lang', 'ZH');
    return {
      url: 'https://api.deepl.com/v2/translate', // DeepL Pro API
      headers: {
        Authorization: `DeepL-Auth-Key ${authKey}`,
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      data: params.toString(),
      responseType: 'json',
    };
  };

  const deeplOfficialRes = res => {
    return res?.translations?.[0]?.text;
  };

  const oaiOffcialReq = (
    text,
    model = 'gpt-3.5-turbo',
    url = 'https://api.openai.com/v1/chat/completions',
    temperature = 0.5,
    maxTokens = 32000
  ) => {
    const authKey = config.deeplxAuthKey.replace(config.authKeyPrefix.openAIOfficial, '');
    return {
      url: url,
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${authKey}`,
      },
      data: JSON.stringify({
        model: model, // 或者您订阅的其他模型,例如 'gpt-4'
        messages: [
          {
            role: 'system',
            content:
              'You are a highly skilled translation engine. Your function is to translate texts accurately into the target {{to}}, maintaining the original format, technical terms, and abbreviations. Do not add any explanations or annotations to the translated text.',
          },
          {
            role: 'user',
            content: `Translate the following source text to ${config.translateTargetLang.openAIOfficial}, Output translation directly without any additional text.\nSource Text: ${text}\nTranslated Text:`,
          },
        ],
        temperature: temperature, // 控制生成内容的随机性,范围是 0 到 1
        max_tokens: maxTokens, // 响应的最大标记数
      }),
      responseType: 'json',
    };
  };

  const oaiOfficalRes = res => {
    return res.choices[0].message.content.trim();
  };

  const translateText = text => {
    const isDeeplOfficial = config.deeplxAuthKey.startsWith(config.authKeyPrefix.deeplOfficial);
    const isOpenAIOfficial = config.deeplxAuthKey.startsWith(config.authKeyPrefix.openAIOfficial);

    let reqData;

    if (!config.deeplxAuthKey) {
      if (!config.deeplxUrl) return '';
      reqData = deeplxReq(text);
    } else if (isDeeplOfficial) {
      reqData = deeplOfficialReq(text);
    } else if (isOpenAIOfficial) {
      reqData = oaiOffcialReq(
        text,
        config.translateModel,
        config.deeplxUrl || 'https://api.openai.com/v1/chat/completions',
        0.5,
        1600
      );
    } else {
      reqData = deeplxLinuxdoReq(text);
    }

    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: 'POST',
        url: reqData.url,
        headers: reqData.headers,
        data: reqData.data,
        responseType: reqData.responseType,
        onload: function (res) {
          console.log('Translation response:', res);
          console.log('Request details:', reqData);

          if (res.status === 200) {
            try {
              const response = typeof res.response === 'string' ? JSON.parse(res.response) : res.response;
              console.log('Parsed response:', response);

              let translation;

              if (isDeeplOfficial) {
                // Pro API 返回格式
                translation = deeplOfficialRes(response);
                console.log('DeepL translation:', translation);
              } else if (isOpenAIOfficial) {
                translation = oaiOfficalRes(response);
                console.log('OAI translation:', translation);
              } else {
                translation = response?.data;
                console.log('DeepLX translation:', translation);
              }

              resolve(translation || '');
            } catch (error) {
              console.error('Error parsing response:', error);
              resolve('');
            }
          } else {
            console.error('Translation failed:', {
              status: res.status,
              statusText: res.statusText,
              response: res.response,
              responseText: res.responseText,
              finalUrl: res.finalUrl,
              headers: res.responseHeaders,
            });
            resolve('');
          }
        },
        onerror: function (err) {
          console.error('Translation error details:', {
            error: err,
            errorText: err.toString(),
            status: err.status,
            statusText: err.statusText,
            responseText: err.responseText,
          });
          resolve('');
        },
      });
    });

    // return GM_xmlhttpRequest({
    //   method: 'POST',
    //   url,
    //   data: JSON.stringify(data),
    //   responseType: 'json',
    //   onload: res => {
    //     console.log(res);
    //     return res.response;
    //   },
    //   onerror: err => {
    //     console.log(err);
    //     return '';
    //   },
    // });

    // return GM.xmlHttpRequest({
    //   method: 'POST',
    //   url,
    //   data: JSON.stringify(data),
    //   responseType: 'json',
    // })
    //   .then(res => {
    //     console.log(res);
    //     console.log(`[翻译结果] ${res.response.data}`)
    //     return res.response.data;
    //   })
    //   .catch(err => {
    //     console.log(err);
    //     return '';
    //   });
  };

  const processTranslateText = async text => {
    // 定义需要保护的块的正则表达式
    const protectedBlocks = [
      // Markdown 代码块
      {
        regex: /```[\s\S]*?```/g,
        type: 'code',
      },
      // BBCode 标签块 (处理嵌套标签)
      {
        regex: /\[(size|spoiler|center|color|grid).*?\][\s\S]*?\[\/\1\]/g,
        type: 'bbcode',
      },
      // 已有的 ruby 标签
      {
        regex: /<ruby>[\s\S]*?<\/ruby>/g,
        type: 'ruby',
      },
      // HTML 标签块
      {
        regex: /<[^>]+>[\s\S]*?<\/[^>]+>/g,
        type: 'html',
      },
      // 图片标签
      {
        regex: /\[image\]\(.*?\)/g,
        type: 'image',
      },
    ];

    // 创建占位符映射
    let placeholders = new Map();
    let placeholderCounter = 0;

    // 保护特殊块
    let processedText = text;
    for (const block of protectedBlocks) {
      processedText = processedText.replace(block.regex, match => {
        const placeholder = `__PROTECTED_${block.type}_${placeholderCounter++}__`;
        placeholders.set(placeholder, match);
        return placeholder;
      });
    }

    // 处理剩余文本
    const segments = processedText.split(/(\n)/);
    let translatedSegments = [];

    for (const segment of segments) {
      if (!segment.trim() || segment === '\n') {
        translatedSegments.push(segment);
        continue;
      }

      // 检查是否是占位符
      if (segment.startsWith('__PROTECTED_')) {
        translatedSegments.push(placeholders.get(segment));
        continue;
      }

      // 翻译普通文本
      const segmentTranslate = await translateText(segment);
      if (config.translateLayout === 'down') {
        translatedSegments.push(`${segment}\n[size=${config.translateSize}]${segmentTranslate}[/size]`);
      } else {
        translatedSegments.push(
          `[size=${config.translateSize}]<ruby><rb>${segment}</rb><rt>${segmentTranslate}</rt></ruby>[/size]`
        );
      }
    }

    // 合并结果
    return translatedSegments.join('');
  };

  const processTextArea = () => {
    let textarea = document.querySelector(`#${uiIDs.replyControl} textarea`);
    let text = textarea.value.trim();
    let originalLength = text.length;

    if (text.length !== 0 && originalLength >= REQUIRED_CHARS) {
      // 检查是否已存在拼音
      const rubyRegex = /(<ruby>[\s\S]*?<\/ruby>)/g;

      // 为中文加入翻译
      if (config.enableTranslate) {
        textarea.value = '开始翻译...';

        processTranslateText(text).then(res => {
          textarea.value = res;

          // 创建并触发 input 事件
          const inputEvent = new Event('input', {
            bubbles: true,
            cancelable: true,
          });
          // 触发事件
          textarea.dispatchEvent(inputEvent);
        });
        return;
      }

      // 为中文加入拼音
      if (!config.enableTranslate && config.enablePinYin) {
        if (containsChinese(text)) {
          // 使用正则表达式将文本分割为已被 <ruby> 包裹的部分和未被包裹的部分
          const parts = text.split(rubyRegex);

          // 处理每一部分
          text = parts
            .map(part => {
              if (rubyRegex.test(part)) {
                // 已被 <ruby> 包裹,保持原样
                return part;
              } else {
                // 未被包裹,进一步分割为中文和非中文
                if (containsChinese(part)) {
                  const segments = part.split(/([\u4e00-\u9fa5]+)/g);
                  return segments
                    .map(segment => {
                      if (containsChinese(segment)) {
                        return convertToRuby(segment);
                      } else {
                        return segment;
                      }
                    })
                    .join('');
                } else {
                  return part;
                }
              }
            })
            .join('');
        }
      }

      textarea.value = text;

      // 创建并触发 input 事件
      const inputEvent = new Event('input', {
        bubbles: true,
        cancelable: true,
      });
      // 触发事件
      textarea.dispatchEvent(inputEvent);
    }
  };

  const handleClick = event => {
    // 修复翻译两次的 BUG
    if (config.enableTranslate) {
      return;
    }

    if (event.target && event.target.closest('button.create')) {
      processTextArea();
    }
  };

  let spacePresses = 0;
  let lastKeyTime = 0;
  let timeoutHandle = null;

  const handleKeydown = event => {
    // console.log(`KeyboardEvent: key='${event.key}' | code='${event.code}'`);

    if (event.ctrlKey && event.key === 'Enter') {
      processTextArea();
      return;
    }

    // 使用 Alt+D 触发翻译
    if (event.altKey && event.keyCode === 68) {
      event.preventDefault(); // 阻止默认行为
      processTextArea();
      return;
    }

    const currentTime = Date.now();
    if (event.code === 'Space') {
      // 如果时间间隔太长,重置计数
      if (currentTime - lastKeyTime > SPACE_PRESS_TIMEOUT) {
        spacePresses = 1;
      } else {
        spacePresses += 1;
      }

      lastKeyTime = currentTime;

      // 清除之前的定时器
      if (timeoutHandle) {
        clearTimeout(timeoutHandle);
      }

      // 设置新的定时器,如果在 SPACE_PRESS_TIMEOUT 毫秒内没有新的按键,则重置计数
      timeoutHandle = setTimeout(() => {
        spacePresses = 0;
      }, SPACE_PRESS_TIMEOUT);

      // 检查是否达到了按键次数
      if (spacePresses === SPACE_PRESS_COUNT) {
        spacePresses = 0; // 重置计数

        // 执行翻译操作
        processTextArea();
      }
    } else {
      // 如果按下了其他键,重置计数
      spacePresses = 0;
      if (timeoutHandle) {
        clearTimeout(timeoutHandle);
        timeoutHandle = null;
      }
    }
  };

  const saveConfig = () => {
    const deeplxUrlInput = document.getElementById(uiIDs.deeplxUrlInput);
    config.deeplxUrl = deeplxUrlInput.value.trim();
    const deeplxAuthKeyInput = document.getElementById(uiIDs.deeplxAuthKeyInput);
    config.deeplxAuthKey = deeplxAuthKeyInput.value.trim();
    const translateModelInput = document.getElementById(uiIDs.translateModelInput);
    config.translateModel = translateModelInput.value;
    const transalteSizeInput = document.getElementById(uiIDs.translateSizeInput);
    config.translateSize = transalteSizeInput.value;
    console.log(config);

    GM_setValue('deeplxUrl', config.deeplxUrl);
    GM_setValue('deeplxAuthKey', config.deeplxAuthKey);
    GM_setValue('enableTranslate', config.enableTranslate);
    GM_setValue('translateModel', config.translateModel);
    GM_setValue('translateSize', config.translateSize);
    GM_setValue('enablePinYin', config.enablePinYin);
    GM_setValue('closeConfigAfterSave', config.closeConfigAfterSave);

    if (config.closeConfigAfterSave) {
      document.getElementById(uiIDs.configPanel).style.display = 'none';
    }
  };

  const restoreDefaults = () => {
    if (confirm('确定要将所有设置恢复为默认值吗?')) {
      config = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
      GM_setValue('maxRetryTimes', config.maxRetryTimes);
      GM_setValue('deeplxUrl', config.deeplxUrl);
      GM_setValue('deeplxAuthKey', config.deeplxAuthKey);
      GM_setValue('authKeyPrefix', config.authKeyPrefix);
      GM_setValue('enableTranslate', config.enableTranslate);
      GM_setValue('translateTargetLang', config.translateTargetLang);
      GM_setValue('translateModel', config.translateModel);
      GM_setValue('translateLayout', config.translateLayout);
      GM_setValue('translateSize', config.translateSize);
      GM_setValue('enablePinYin', config.enablePinYin);
      GM_setValue('closeConfigAfterSave', config.closeConfigAfterSave);

      const panel = document.getElementById(uiIDs.configPanel);
      if (panel) {
        updateConfigPanelContent(panel);
      }
    }
  };

  const createCheckbox = (id, text, checked, onChange = undefined) => {
    const label = document.createElement('label');
    label.style.display = 'flex';
    label.style.alignItems = 'center';
    label.style.marginBottom = '10px';
    label.style.cursor = 'pointer';

    const checkbox = document.createElement('input');
    checkbox.type = 'checkbox';
    checkbox.id = id;
    checkbox.checked = checked;
    checkbox.style.marginRight = '10px';
    if (onChange !== undefined) {
      checkbox.addEventListener('change', e => onChange(e));
    } else {
      checkbox.addEventListener('change', e => {
        config[id] = e.target.checked;
        GM_setValue(id, config[id]);
      });
    }

    label.appendChild(checkbox);
    label.appendChild(document.createTextNode(text));

    return label;
  };

  const createSelect = (id, labelText, options, defaultValue) => {
    const container = document.createElement('div');
    container.style.display = 'flex';
    container.style.alignItems = 'center';
    container.style.marginBottom = '10px';

    const label = document.createElement('label');
    label.htmlFor = id;
    label.textContent = labelText;
    label.style.marginRight = '10px';

    const select = document.createElement('select');
    select.id = id;
    select.style.flex = '1';

    options.forEach(option => {
      const optionElement = document.createElement('option');
      optionElement.value = option.value;
      optionElement.textContent = option.text;
      select.appendChild(optionElement);
    });

    select.value = defaultValue;

    select.addEventListener('change', e => {
      config[id] = e.target.value;
      GM_setValue(id, config[id]);
    });

    container.appendChild(select);
    container.appendChild(label);

    return container;
  };

  const createTextInput = (id, value, labelText, placeholder, type = 'text') => {
    const container = document.createElement('div');
    container.style.marginBottom = '15px';

    const label = document.createElement('label');
    label.textContent = labelText;
    label.style.display = 'block';
    label.style.marginBottom = '5px';
    container.appendChild(label);

    const inputPlace = document.createElement('input');
    inputPlace.id = id;
    inputPlace.type = type;
    inputPlace.value = value;
    inputPlace.placeholder = placeholder;
    inputPlace.style.width = '100%';
    inputPlace.style.padding = '5px';
    inputPlace.style.border = '1px solid var(--panel-border)';
    inputPlace.style.borderRadius = '4px';
    inputPlace.style.backgroundColor = 'var(--panel-bg)';
    inputPlace.style.color = 'var(--panel-text)';
    container.appendChild(inputPlace);

    return [container, inputPlace];
  };

  const createTextArea = (id, value, labelText, placeholder) => {
    const container = document.createElement('div');
    container.style.marginBottom = '15px';

    const label = document.createElement('label');
    label.textContent = labelText;
    label.style.display = 'block';
    label.style.marginBottom = '5px';
    container.appendChild(label);

    const textarea = document.createElement('textarea');
    textarea.id = id;
    if (typeof value === 'string') {
      textarea.value = value;
    } else {
      textarea.value = JSON.stringify(value, null, 2);
    }
    textarea.placeholder = placeholder;
    textarea.rows = 5;
    textarea.style.width = '100%';
    textarea.style.padding = '5px';
    textarea.style.border = '1px solid var(--panel-border)';
    textarea.style.borderRadius = '4px';
    textarea.style.backgroundColor = 'var(--panel-bg)';
    textarea.style.color = 'var(--panel-text)';
    container.appendChild(textarea);

    return [container, textarea];
  };

  const createButton = (text, onClick, primary = false) => {
    const button = document.createElement('button');
    button.textContent = text;
    button.style.padding = '8px 16px';
    button.style.border = 'none';
    button.style.borderRadius = '4px';
    button.style.cursor = 'pointer';
    button.style.backgroundColor = primary ? '#0078d7' : '#f0f0f0';
    button.style.color = primary ? '#ffffff' : '#333333';
    button.addEventListener('click', onClick);
    return button;
  };

  const updateConfigPanelContent = panel => {
    panel.innerHTML = '';

    const title = document.createElement('h3');
    title.textContent = '配置';
    title.style.marginTop = '0';
    title.style.marginBottom = '15px';
    panel.appendChild(title);

    panel.appendChild(createTextInput(uiIDs.deeplxUrlInput, config.deeplxUrl, 'URL', '填写自定义请求地址')[0]);
    panel.appendChild(
      createTextInput(uiIDs.deeplxAuthKeyInput, config.deeplxAuthKey, 'Auth Key', 'connect的key/deepl官key/oai的key')[0]
    );
    panel.appendChild(createTextInput(uiIDs.translateModelInput, config.translateModel, 'Model', '填写可用模型')[0]);
    panel.appendChild(
      createTextInput(
        uiIDs.translateSizeInput,
        config.translateSize,
        '翻译字体大小(百分比)',
        '默认值为150(字体大小为原始的150%)',
        'number'
      )[0]
    );
    panel.appendChild(
      createSelect(
        'translateLayout',
        '翻译布局(上/下)',
        [
          { text: '上', value: 'up' },
          { text: '下', value: 'down' },
        ],
        config.translateLayout
      )
    );
    panel.appendChild(createCheckbox('enableTranslate', '启用翻译', config.enableTranslate));
    if (checkPinYinRequire()) {
      panel.appendChild(createCheckbox('enablePinYin', '启用拼音注音', config.enablePinYin));
    }
    panel.appendChild(createCheckbox('closeConfigAfterSave', '保存后自动关闭配置页面', config.closeConfigAfterSave));

    const buttonContainer = document.createElement('div');
    buttonContainer.style.display = 'flex';
    buttonContainer.style.justifyContent = 'space-between';
    buttonContainer.style.marginTop = '20px';

    const restoreDefaultsButton = createButton('恢复默认设置', restoreDefaults);
    restoreDefaultsButton.style.marginRight = '10px';

    const saveButton = createButton('保存设置', saveConfig, true);
    const closeButton = createButton('关闭', () => {
      panel.style.display = 'none';
    });

    buttonContainer.appendChild(restoreDefaultsButton);
    buttonContainer.appendChild(saveButton);
    buttonContainer.appendChild(closeButton);
    panel.appendChild(buttonContainer);
  };

  const createConfigPanel = () => {
    // 获取页面的 <meta name="theme-color"> 标签
    const themeColorMeta = document.querySelector('meta[name="theme-color"]');
    let themeColor = '#ffffff'; // 默认白色背景
    let invertedColor = '#000000'; // 默认黑色字体

    if (themeColorMeta) {
      themeColor = themeColorMeta.getAttribute('content');
      invertedColor = getInvertColor(themeColor); // 计算相反颜色
    }

    // 设置样式变量
    const style = document.createElement('style');
    style.textContent = `
      :root {
          --panel-bg: ${themeColor};
          --panel-text: ${invertedColor};
          --panel-border: ${invertedColor};
          --button-bg: ${invertedColor};
          --button-text: ${themeColor};
          --button-hover-bg: ${getInvertColor(invertedColor)};
      }`;
    document.head.appendChild(style);

    const panel = document.createElement('div');
    panel.id = uiIDs.configPanel;
    panel.style.position = 'fixed';
    panel.style.top = '80px';
    panel.style.right = '360px';
    panel.style.padding = '20px';
    panel.style.border = `1px solid var(--panel-border)`;
    panel.style.borderRadius = '8px';
    panel.style.zIndex = '10000';
    panel.style.width = '300px';
    panel.style.maxHeight = '80%';
    panel.style.overflowY = 'auto';
    panel.style.display = 'none'; // 默认隐藏面板
    panel.style.backgroundColor = 'var(--panel-bg)';
    panel.style.color = 'var(--panel-text)';
    panel.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)';

    updateConfigPanelContent(panel);

    document.body.appendChild(panel);
    return panel;
  };

  const toggleConfigPanel = () => {
    let panel = document.getElementById(uiIDs.configPanel);
    panel = panel || createConfigPanel();
    panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
  };

  const createConfigButton = () => {
    const toolbar = document.querySelector('.d-editor-button-bar');
    if (!toolbar || document.getElementById(uiIDs.configButton)) return;

    const configButton = document.createElement('button');
    configButton.id = uiIDs.configButton;
    configButton.className = 'btn btn-flat btn-icon no-text user-menu-tab active';
    configButton.title = '配置';
    configButton.innerHTML =
      '<svg class="fa d-icon d-icon-discourse-other-tab svg-icon svg-string" xmlns="http://www.w3.org/2000/svg"><use href="#discourse-other-tab"></use></svg>';
    configButton.onclick = toggleConfigPanel;

    toolbar.appendChild(configButton);
  };

  const watchReplyControl = () => {
    const replyControl = document.getElementById(uiIDs.replyControl);
    if (!replyControl) return;

    const observer = new MutationObserver(mutations => {
      mutations.forEach(mutation => {
        if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
          if (replyControl.classList.contains('closed')) {
            const panel = document.getElementById(uiIDs.configPanel);
            if (panel) {
              panel.style.display = 'none';
            }
          } else {
            // 当 reply-control 重新打开时,尝试添加配置按钮
            setTimeout(createConfigButton, 500); // 给予一些时间让编辑器完全加载
          }
        }
      });
    });

    observer.observe(replyControl, { attributes: true });
  };

  const watchForEditor = () => {
    const observer = new MutationObserver(mutations => {
      mutations.forEach(mutation => {
        if (mutation.type === 'childList') {
          const addedNodes = mutation.addedNodes;
          for (let node of addedNodes) {
            if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('d-editor')) {
              createConfigButton();
              return;
            }
          }
        }
      });
    });

    observer.observe(document.body, { childList: true, subtree: true });
  };

  const init = () => {
    const container = document.getElementById(uiIDs.replyControl);
    container.addEventListener('click', handleClick, true);
    document.addEventListener('keydown', handleKeydown, true);
    if (!document.getElementById(uiIDs.configButton)) {
      createConfigButton();
    }
    watchReplyControl();
    watchForEditor();
  };

  // 初始化
  setTimeout(() => {
    init();
  }, 1000);
})();