伪物弹幕

此乃伪物。

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

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

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==UserScript==
// @name         伪物弹幕
// @namespace    http://tampermonkey.net/
// @version      1.20
// @description  此乃伪物。
// @author       Yora
// @license      MIT
// @match        *://live.bilibili.com/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @connect      generativelanguage.googleapis.com
// @run-at       document-idle
// ==/UserScript==

(function() {
  'use strict';

  try {
  const DEFAULT = {
    n: 15,
    autoSend: false,
    apiKey: '',
    danmuInputSelector: 'textarea',
    danmuSendButtonSelector: 'button[data-send]',
    interval: 0,
    keywords: '',
    // 新增默认设置
    stealMode: false,
    focusMode: false,
    stealUids: '',
  };

  const DEFAULT_PROMPT = `背景:我正在一个直播间里,作为粉丝,请在生成弹幕时,完全融入弹幕氛围,保持简短且口语化的风格。避免使用正式、书面化或AI的表达,不要带感叹词,不要透露是AI。适度复读和互动,让回复像真实观众发出的弹幕。请根据以下弹幕列表,生成一句相关且自然的弹幕内容,不要带发言人或冒号,也不要多余解释。弹幕列表:
{danmu}`;

  GM_addStyle(`
    #llm-panel {
      position: fixed;
      right: 20px;
      bottom: 20px;
      width: 380px;
      background: #fff8f7;
      color: #3b1e1e;
      padding: 12px 16px;
      border-radius: 10px;
      font-size: 14px;
      font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
      box-shadow: 0 8px 20px rgba(255, 200, 160, 0.5);
      z-index: 9999999;
      user-select: none;
      cursor: default;
      transition: height 0.3s ease, width 0.3s ease, padding 0.3s ease;
      overflow: hidden;
    }
    #llm-panel header {
      font-weight: 700;
      font-size: 18px;
      margin-bottom: 8px;
      cursor: move;
      user-select: none;
      color: #a82f2f;
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding-right: 6px;
    }
    #llm-toggle-minimize {
      background: transparent;
      border: none;
      color: #a82f2f;
      font-size: 20px;
      cursor: pointer;
      user-select: none;
      line-height: 1;
      padding: 0 6px;
      flex-shrink: 0;
    }
    #llm-toggle-options {
      background: transparent;
      border: none;
      color: #a85a32;
      font-size: 13px;
      text-decoration: underline;
      cursor: pointer;
      margin-bottom: 10px;
      user-select: none;
      padding: 0;
    }
    #llm-options {
      display: none;
      margin-bottom: 10px;
    }
    #llm-options label {
      display: block;
      margin-top: 10px;
      font-weight: 600;
      cursor: pointer;
      user-select: none;
      color: #5f3b3b;
    }
    #llm-options input[type="number"],
    #llm-options input[type="text"],
    #llm-options textarea {
      width: 100%;
      padding: 8px 12px;
      margin-top: 6px;
      border-radius: 6px;
      border: 1px solid #f1c8c8;
      background: #fff1f0;
      color: #5f3b3b;
      font-size: 13px;
      box-sizing: border-box;
      transition: border-color 0.3s;
      user-select: text;
      font-family: monospace;
      resize: vertical;
    }
    #llm-options input[type="number"]:focus,
    #llm-options input[type="text"]:focus,
    #llm-options textarea:focus {
      border-color: #a84747;
      outline: none;
      background: #ffe5e5;
    }
    #llm-options input[type="checkbox"] {
      margin-right: 6px;
      cursor: pointer;
      vertical-align: middle;
      width: 18px;
      height: 18px;
      user-select: none;
    }
    #llm-buttons {
      margin-top: 6px;
      user-select: none;
    }
    #llm-buttons button {
      margin: 6px 10px 0 0;
      padding: 10px 18px;
      border: none;
      border-radius: 8px;
      background: #f9c06b;
      color: #5f3b3b;
      font-weight: 700;
      font-size: 14px;
      cursor: pointer;
      box-shadow: 0 4px 8px rgba(249,192,107,0.6);
      transition: background 0.3s, color 0.3s;
      user-select: none;
    }
    #llm-buttons button:hover:not(:disabled) {
      background: #fbcf83;
      color: #4b2c2c;
    }
    #llm-buttons button:disabled {
      cursor: not-allowed;
      opacity: 0.6;
      background: #d8b25e;
      color: #3c2e22;
      box-shadow: none;
    }
    #llm-buttons button.running {
      background: #c94b4b !important;
      color: #fff !important;
      box-shadow: 0 0 12px #c94b4bcc !important;
    }
    #llm-status {
      max-height: 150px;
      overflow-y: auto;
      background: #fff2f0;
      border-radius: 10px;
      padding: 12px;
      font-family: "Consolas", "Courier New", monospace;
      font-size: 13px;
      line-height: 1.4;
      color: #4a2929;
      white-space: pre-wrap;
      box-shadow: inset 0 0 10px #f9c06baa;
      user-select: text;
      transition: max-height 0.3s ease, overflow 0.3s ease;
    }
    /* 滚动条美化 */
    #llm-status::-webkit-scrollbar {
      width: 8px;
    }
    #llm-status::-webkit-scrollbar-track {
      background: transparent;
    }
    #llm-status::-webkit-scrollbar-thumb {
      background: #f9c06b;
      border-radius: 4px;
    }
  `);

  // 创建面板HTML,新增“定时触发”开关,和自动发送同级
  const panel = document.createElement('div');
  panel.id = 'llm-panel';
  panel.innerHTML = `
    <header id="llm-header">
      伪物弹幕
      <button id="llm-toggle-minimize" title="缩小/展开">—</button>
    </header>
    <div id="llm-content">
      <button id="llm-toggle-options">展开更多选项 ▼</button>
      <div id="llm-options">
        <label for="llm-n">参考弹幕数量 (n):
          <input id="llm-n" type="number" min="1" max="100" />
        </label>
        <label for="llm-apikey">API Key:
          <input id="llm-apikey" type="text" placeholder="AIza..." autocomplete="off" />
        </label>
        <label><input id="llm-autosend" type="checkbox" /> 自动发送弹幕</label>

        <!-- 新增:偷子模式 & 专注模式 -->
        <label><input id="llm-stealmode" type="checkbox" /> 偷子模式</label>
        <div id="llm-focus-row" style="display:none; margin-left:18px;">
          <label><input id="llm-focusmode" type="checkbox" /> 专注模式</label>
        </div>
        <label id="llm-steal-uids-label" style="display:none;">优先偷取 UID 列表(逗号分隔):
          <input id="llm-steal-uids" type="text" placeholder="14999357,114514" />
        </label>

        <label><input id="llm-timer" type="checkbox" /> 定时触发</label>
        <label for="llm-interval">定时间隔(秒):
          <input id="llm-interval" type="number" min="1" max="3600" />
        </label>
        <label for="llm-keywords">关键词触发(逗号分隔):
          <input id="llm-keywords" type="text" placeholder="关键字1,关键字2,关键字3" />
        </label>
        <label for="llm-customprompt">自定义提示词(支持使用{danmu}作为弹幕列表占位符):
          <textarea id="llm-customprompt" rows="10" placeholder="请输入自定义提示词,{danmu} 将替换为弹幕列表"></textarea>
        </label>
      </div>
      <div id="llm-buttons">
        <button id="llm-run">仿写弹幕</button>
        <button id="llm-save" style="display:none;">保存配置</button>
        <button id="llm-showdanmu">弹幕列表</button>
      </div>
    </div>
    </br>
    <div id="llm-status">状态信息将在这里显示</div>
  `;
  document.body.appendChild(panel);

  // 折叠展开更多选项(不动)
  const toggleBtn = document.getElementById('llm-toggle-options');
  const optionsDiv = document.getElementById('llm-options');
  toggleBtn.addEventListener('click', () => {
    if (optionsDiv.style.display === 'none' || optionsDiv.style.display === '') {
      optionsDiv.style.display = 'block';
      toggleBtn.textContent = '收起更多选项 ▲';
    } else {
      optionsDiv.style.display = 'none';
      toggleBtn.textContent = '展开更多选项 ▼';
    }
  });
  optionsDiv.style.display = 'none';

  // 拖动逻辑(保持不变)
  const header = document.getElementById('llm-header');
  const panelElement = document.getElementById('llm-panel');
  let isDragging = false;
  let dragStartX = 0;
  let dragStartY = 0;
  let panelStartRight = 0;
  let panelStartBottom = 0;

  header.addEventListener('mousedown', e => {
    if (e.target.id === 'llm-toggle-minimize') return;
    isDragging = true;
    dragStartX = e.clientX;
    dragStartY = e.clientY;
    const rect = panelElement.getBoundingClientRect();
    panelStartRight = window.innerWidth - rect.right;
    panelStartBottom = window.innerHeight - rect.bottom;
    document.body.style.userSelect = 'none';
  });
  document.addEventListener('mouseup', () => {
    isDragging = false;
    document.body.style.userSelect = '';
  });
  document.addEventListener('mousemove', e => {
    if (!isDragging) return;
    const dx = e.clientX - dragStartX;
    const dy = e.clientY - dragStartY;
    panelElement.style.right = (panelStartRight - dx) + 'px';
    panelElement.style.bottom = (panelStartBottom - dy) + 'px';
  });

  // 缩小展开按钮(保持你原来逻辑不变)
  const llmContent = document.getElementById('llm-content');
  const toggleMinBtn = document.getElementById('llm-toggle-minimize');
  const statusDiv = document.getElementById('llm-status');

  let minimized = false;
  function setMinimized(state) {
    minimized = state;
    if (minimized) {
      llmContent.style.display = 'none';
      statusDiv.style.maxHeight = '40px';
      statusDiv.style.overflow = 'hidden';
      toggleMinBtn.textContent = '+';
      panelElement.style.height = 'auto';
      panelElement.style.width = '280px';
      panelElement.style.padding = '8px 12px';
    } else {
      llmContent.style.display = 'block';
      statusDiv.style.maxHeight = '150px';
      statusDiv.style.overflowY = 'auto';
      toggleMinBtn.textContent = '—';
      panelElement.style.height = 'auto';
      panelElement.style.width = '380px';
      panelElement.style.padding = '12px 16px';
    }
  }
  toggleMinBtn.addEventListener('click', e => {
    e.stopPropagation();
    setMinimized(!minimized);
  });
  setMinimized(false);

  // 状态更新函数
  function updateStatus(text) {
    statusDiv.textContent = text;
  }

  // 定时相关变量
  let timerIsRunning = false;
  let intervalId = null;

  // 新增倒计时变量
  let countdownTimerId = null;
  let nextTriggerTime = 0;

  // 获取元素
  const timerCheckbox = document.getElementById('llm-timer');
  const runBtn = document.getElementById('llm-run');
  const intervalInput = document.getElementById('llm-interval');
  const autoSendCheckbox = document.getElementById('llm-autosend');

  // 新增元素引用
  const stealCheckbox = document.getElementById('llm-stealmode');
  const focusRow = document.getElementById('llm-focus-row');
  const focusCheckbox = document.getElementById('llm-focusmode');
  const stealUidsLabel = document.getElementById('llm-steal-uids-label');
  const stealUidsInput = document.getElementById('llm-steal-uids');

  // === 以下是你已有的调用接口函数,保持不变 ===
  async function collectRecentDanmu() {
    const n = Number(await GM_getValue('n', DEFAULT.n));
    const nodes = Array.from(document.querySelectorAll('.chat-item.danmaku-item'));
    return nodes.slice(-n).map(el => {
      const danmuSpan = el.querySelector('span.danmaku-item-right');
      return danmuSpan ? danmuSpan.textContent.trim() : '';
    }).filter(Boolean);
  }

  // 新增:按对象返回最近弹幕(包含 uid 与 text),不替换原有函数
  async function collectRecentDanmuObjs() {
    const n = Number(await GM_getValue('n', DEFAULT.n));
    const nodes = Array.from(document.querySelectorAll('.chat-item.danmaku-item'));
    const sliced = nodes.slice(-n);
    return sliced.map(el => {
      const danmuSpan = el.querySelector('span.danmaku-item-right');
      const text = danmuSpan ? danmuSpan.textContent.trim() : '';
      const uid = el.getAttribute('data-uid') || '';
      return { uid: uid, text: text, el: el };
    }).filter(o => o.text);
  }

  function callGemini(prompt) {
    return new Promise(async (resolve, reject) => {
      const apiKey = await GM_getValue('apiKey', DEFAULT.apiKey);
      if (!apiKey) return reject(new Error('未设置 API Key'));

      const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`;

      const bodyObj = {
        contents: [
          {
            parts: [
              { text: prompt }
            ]
          }
        ],
        generationConfig: {
          temperature: 0.7,
          maxOutputTokens: 5000
        }
      };

      const body = JSON.stringify(bodyObj);

      GM_xmlhttpRequest({
        method: 'POST',
        url,
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json',
          'User-Agent': 'Mozilla/5.0'
        },
        data: body,
        onload: res => {
          try {
            const data = JSON.parse(res.responseText);
            if (data?.error) {
              return reject(new Error(`API 错误: ${data.error.message}`));
            }
            let output = '';
            if (data?.candidates?.length) {
              const parts = data.candidates[0].content?.parts;
              if (parts && parts.length) {
                output = parts.map(p => p.text).join('');
              }
            }
            if (!output) {
              return resolve('[模型未返回内容,可能因安全策略或无法理解当前弹幕]');
            }
            resolve(output);
          } catch (e) {
            reject(new Error(`解析响应失败: ${e.message}`));
          }
        },
        onerror: err => reject(new Error(`网络请求失败: ${err.message}`))
      });
    });
  }

  async function fillAndMaybeSend(text) {
    if (!text || text.includes('[模型未返回内容')) return;

    const input = document.querySelector(DEFAULT.danmuInputSelector);
    if (!input) return;

    input.focus();
    input.value = text;
    input.dispatchEvent(new Event('input', { bubbles: true }));
    input.dispatchEvent(new Event('change', { bubbles: true }));

    if (await GM_getValue('autoSend', DEFAULT.autoSend)) {
      // 等 100 毫秒再点击发送按钮,确保输入事件完成
      await new Promise(r => setTimeout(r, 100));

      const btn = Array.from(document.querySelectorAll('button'))
        .find(b => b.textContent.trim() === '发送' && !b.disabled);

      if (btn) {
        btn.click();
      } else {
        updateStatus('未找到发送按钮');
      }
    }
  }

  // 新增:偷子模式的触发逻辑
  async function triggerStealMode() {
    // 获取最近弹幕对象
    const objs = await collectRecentDanmuObjs();
    if (!objs || objs.length === 0) {
      updateStatus('未找到弹幕(偷子模式)');
      return;
    }
    // 如果专注模式且有 UID 列表则按 UID 顺序优先取
    const focus = await GM_getValue('focusMode', DEFAULT.focusMode);
    const uidStr = (await GM_getValue('stealUids', DEFAULT.stealUids)) || stealUidsInput.value || '';
    const uidList = uidStr.split(',').map(s => s.trim()).filter(Boolean);

    let chosen = null;
    if (focus && uidList.length > 0) {
      // 按 uidList 的顺序,找该 uid 的最新弹幕(从后往前遍历)
      for (let uid of uidList) {
        for (let i = objs.length - 1; i >= 0; i--) {
          if (objs[i].uid === uid) {
            chosen = objs[i];
            break;
          }
        }
        if (chosen) break;
      }
    }
    if (!chosen) {
      // 随机取一个(从 objs 中随机选择)
      const idx = Math.floor(Math.random() * objs.length);
      chosen = objs[idx];
    }
    if (chosen && chosen.text) {
      await fillAndMaybeSend(chosen.text);
      updateStatus('偷子模式已发送(来源 uid:' + (chosen.uid || 'unknown') + '):\n' + chosen.text);
    } else {
      updateStatus('未能选到合适弹幕(偷子模式)');
    }
  }

  async function triggerCallGemini() {
    updateStatus('调用接口中...');
    const danmuList = await collectRecentDanmu();
    if (danmuList.length === 0) {
      updateStatus('未找到弹幕');
      return;
    }

    // 如果开启偷子模式则走偷子逻辑(不请求 LLM)
    const steal = await GM_getValue('stealMode', DEFAULT.stealMode);
    if (steal) {
      await triggerStealMode();
      return;
    }

    let customPromptTemplate = await GM_getValue('customPrompt', '');
    if (!customPromptTemplate) customPromptTemplate = DEFAULT_PROMPT;

    const prompt = customPromptTemplate.replace(/\{danmu\}/g, danmuList.join('\n'));

    try {
      const respText = await callGemini(prompt);
      await fillAndMaybeSend(respText);
      updateStatus('完成,模型回复:\n' + respText);
    } catch (e) {
      updateStatus('请求失败:' + e.message);
    }
  }
  // === 调用接口函数结束 ===

  // 倒计时更新函数
  function updateCountdown() {
    if (!timerIsRunning) return;
    const now = Date.now();
    let remain = Math.floor((nextTriggerTime - now) / 1000);
    if (remain < 0) remain = 0;
    updateStatus(`距离下次定时触发还有 ${remain} 秒`);
    if (remain <= 0) {
      clearInterval(countdownTimerId);
      countdownTimerId = null;
    }
  }

  function updateRunButton() {
    if (!timerCheckbox.checked) {
      runBtn.textContent = '仿写弹幕';
      runBtn.classList.remove('running');
      timerIsRunning = false;
      if (intervalId) {
        clearInterval(intervalId);
        intervalId = null;
      }
      if (countdownTimerId) {
        clearInterval(countdownTimerId);
        countdownTimerId = null;
      }
      updateStatus('定时触发已关闭');
    } else {
      runBtn.textContent = '开始定时触发';
      runBtn.classList.remove('running');
      timerIsRunning = false;
      if (intervalId) {
        clearInterval(intervalId);
        intervalId = null;
      }
      if (countdownTimerId) {
        clearInterval(countdownTimerId);
        countdownTimerId = null;
      }
      updateStatus('定时触发未运行');
    }
  }

  // 点击“仿写弹幕”或“开始/停止定时触发”按钮
  runBtn.addEventListener('click', async () => {
    if (timerCheckbox.checked) {
      // 定时模式开关开启,点击为启动/停止定时触发
      if (timerIsRunning) {
        // 停止
        timerIsRunning = false;
        runBtn.textContent = '开始定时触发';
        runBtn.classList.remove('running');
        if (intervalId) {
          clearInterval(intervalId);
          intervalId = null;
        }
        if (countdownTimerId) {
          clearInterval(countdownTimerId);
          countdownTimerId = null;
        }
        updateStatus('已停止定时触发');
      } else {
        // 开始定时触发
        const intervalSeconds = parseInt(intervalInput.value, 10);
        if (isNaN(intervalSeconds) || intervalSeconds <= 0) {
          alert('请填写有效的定时间隔(秒)');
          return;
        }
        timerIsRunning = true;
        runBtn.textContent = '停止定时触发';
        runBtn.classList.add('running');
        nextTriggerTime = Date.now() + intervalSeconds * 1000;

        // 先立即触发一次
        try {
          await triggerCallGemini();
        } catch (e) {
          updateStatus('触发失败:' + e.message);
        }
        // 设置定时器周期触发
        intervalId = setInterval(async () => {
          nextTriggerTime = Date.now() + intervalSeconds * 1000;
          try {
            // 先检查关键词触发(如果有设置)
            const keywordsStr = document.getElementById('llm-keywords').value.trim();
            if (keywordsStr) {
              const keywords = keywordsStr.split(',').map(s => s.trim()).filter(Boolean);
              const danmus = await collectRecentDanmu();
              const danmuText = danmus.join(' ');
              // 任意关键词存在则触发
              const matched = keywords.some(k => danmuText.includes(k));
              if (!matched) {
                updateStatus(`定时触发跳过(无匹配关键词)`);
                return; // 跳过本次触发
              }
            }
            // 如果偷子模式开启,triggerCallGemini 内部会处理
            await triggerCallGemini();
          } catch (e) {
            updateStatus('定时触发失败:' + e.message);
          }
        }, intervalSeconds * 1000);

        // 启动倒计时显示
        if (countdownTimerId) clearInterval(countdownTimerId);
        countdownTimerId = setInterval(updateCountdown, 1000);
      }
    } else {
      // 非定时模式,直接单次触发
      runBtn.disabled = true;
      updateStatus('调用中...');
      try {
        await triggerCallGemini();
      } catch (e) {
        updateStatus('调用失败:' + e.message);
      }
      runBtn.disabled = false;
    }
  });

  // 读取配置初始化控件值
  (async () => {
    document.getElementById('llm-n').value = await GM_getValue('n', DEFAULT.n);
    document.getElementById('llm-apikey').value = await GM_getValue('apiKey', DEFAULT.apiKey);
    document.getElementById('llm-autosend').checked = await GM_getValue('autoSend', DEFAULT.autoSend);
    document.getElementById('llm-interval').value = await GM_getValue('interval', 0);
    document.getElementById('llm-keywords').value = await GM_getValue('keywords', '');
    document.getElementById('llm-customprompt').value = await GM_getValue('customPrompt', DEFAULT_PROMPT);

    // 读取新增设置
    const stealVal = await GM_getValue('stealMode', DEFAULT.stealMode);
    stealCheckbox.checked = !!stealVal;
    const focusVal = await GM_getValue('focusMode', DEFAULT.focusMode);
    focusCheckbox.checked = !!focusVal;
    document.getElementById('llm-steal-uids').value = await GM_getValue('stealUids', DEFAULT.stealUids);

    // 根据偷子模式显示专注行
    if (stealCheckbox.checked) {
      focusRow.style.display = 'block';
      stealUidsLabel.style.display = 'block';
    } else {
      focusRow.style.display = 'none';
      stealUidsLabel.style.display = 'none';
    }

    timerCheckbox.checked = false;
    updateRunButton();
  })();

  // 监听输入框变化并保存到GM存储
  document.getElementById('llm-n').addEventListener('change', e => {
    const val = parseInt(e.target.value, 10);
    if (val > 0 && val <= 100) GM_setValue('n', val);
  });
  document.getElementById('llm-apikey').addEventListener('change', e => {
    GM_setValue('apiKey', e.target.value.trim());
  });
  document.getElementById('llm-autosend').addEventListener('change', e => {
    GM_setValue('autoSend', e.target.checked);
  });
  intervalInput.addEventListener('change', e => {
    const val = parseInt(e.target.value, 10);
    if (val > 0 && val <= 3600) GM_setValue('interval', val);
  });
  document.getElementById('llm-keywords').addEventListener('change', e => {
    GM_setValue('keywords', e.target.value.trim());
  });
  document.getElementById('llm-customprompt').addEventListener('change', e => {
    GM_setValue('customPrompt', e.target.value);
  });

  // 新增:偷子与专注设置保存与展示联动
  stealCheckbox.addEventListener('change', async (e) => {
    const checked = e.target.checked;
    await GM_setValue('stealMode', checked);
    if (checked) {
      focusRow.style.display = 'block';
      stealUidsLabel.style.display = 'block';
    } else {
      focusRow.style.display = 'none';
      stealUidsLabel.style.display = 'none';
    }
  });
  focusCheckbox.addEventListener('change', async (e) => {
    const checked = e.target.checked;
    await GM_setValue('focusMode', checked);
  });
  stealUidsInput.addEventListener('change', async (e) => {
    await GM_setValue('stealUids', e.target.value.trim());
  });

  timerCheckbox.addEventListener('change', e => {
    updateRunButton();
  });

  // “弹幕列表”按钮,显示最近弹幕
  document.getElementById('llm-showdanmu').addEventListener('click', async () => {
    const list = await collectRecentDanmu();
    if (list.length === 0) {
      alert('未找到弹幕');
    } else {
      alert('最近弹幕(最新在最后):\n\n' + list.join('\n'));
    }
  });

  // “保存配置”按钮隐藏,保持界面简洁,不再显示
  document.getElementById('llm-save').style.display = 'none';

  } catch (outerErr) {
    // 全局保护,防止脚本因错误中断
    console.error('伪物弹幕脚本发生未捕获错误:', outerErr);
    try { // 尝试简单展示到面板(若面板已创建)
      const s = document.getElementById('llm-status');
      if (s) s.textContent = '脚本错误:' + outerErr.message;
    } catch (e) {}
  }

})();