Greasy Fork

Greasy Fork is available in English.

YouTube Mobile 评论自动@用户名+引用 (精准修复版)

自动插入“@用户名”,可选择是否引用原评论。修复抓取到时间戳的问题。

当前为 2025-12-06 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         YouTube Mobile 评论自动@用户名+引用 (精准修复版)
// @namespace    yt-mobile-autoreply-vm
// @version      2.1
// @description  自动插入“@用户名”,可选择是否引用原评论。修复抓取到时间戳的问题。
// @match        https://m.youtube.com/*
// @run-at       document-idle
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_notification
// @license      MIT
// ==/UserScript==

(function() {
  'use strict';

  // ====== 1. 配置管理 (开关) ======
  const KEY_ENABLE_QUOTE = 'enable_quote';
  // 默认关闭引用 (false),如需默认开启请改为 true
  let isQuoteEnabled = GM_getValue(KEY_ENABLE_QUOTE, false);

  // 注册菜单命令
  function registerMenu() {
    // 动态生成菜单标题
    const statusIcon = isQuoteEnabled ? '✅' : '⬜'; 
    const menuTitle = `${statusIcon} 开启引用原评论功能`;
    
    GM_registerMenuCommand(menuTitle, () => {
      isQuoteEnabled = !isQuoteEnabled;
      GM_setValue(KEY_ENABLE_QUOTE, isQuoteEnabled);
      
      // 简单的提示反馈
      showDebugMsg(`引用模式已${isQuoteEnabled ? '开启' : '关闭'} (下次回复生效)`);
      
      // 刷新页面以更新菜单文字 (Violentmonkey特性)
      // 如果不想刷新,可以注释掉下面这行,但菜单文字不会马上变
      setTimeout(() => location.reload(), 500);
    });
  }
  registerMenu(); // 初始化菜单

  // ====== 2. 简易消息提示 ======
  function showDebugMsg(msg) {
    let box = document.getElementById('yt-reply-debug');
    if (!box) {
      box = document.createElement('div');
      box.id = 'yt-reply-debug';
      Object.assign(box.style, {
        position: 'fixed',
        bottom: '80px', // 避开底部导航栏
        left: '50%',
        transform: 'translateX(-50%)',
        background: 'rgba(0,0,0,0.85)',
        color: '#fff',
        fontSize: '13px',
        padding: '8px 12px',
        borderRadius: '20px',
        zIndex: 999999,
        pointerEvents: 'none',
        textAlign: 'center',
        maxWidth: '90%',
        boxShadow: '0 2px 5px rgba(0,0,0,0.3)'
      });
      document.body.appendChild(box);
    }
    box.textContent = msg;
    box.style.opacity = '1';
    
    // 防抖动计时器
    if (box.timer) clearTimeout(box.timer);
    box.timer = setTimeout(() => {
      box.style.opacity = '0';
    }, 2500);
  }

  // 全局变量存储点击信息
  let lastClickedUser = null;
  let lastClickedText = null;

  // ====== 3. 核心:精准抓取内容 ======
  
  // 抓取用户名
  function extractUsername(comment) {
    if (!comment) return null;
    // 常见用户名容器
    const selectors = [
      '.comment-header .author-text',
      '.YtmCommentRendererTitle', 
      'a[href*="/@"]',
      '.comment-title'
    ];
    for (const s of selectors) {
      const el = comment.querySelector(s);
      if (el && el.textContent.trim()) return el.textContent.trim();
    }
    return null;
  }

  // 抓取评论内容 (针对性修复)
  function extractCommentText(comment) {
    if (!comment) return null;

    let el = null;

    // 策略 A (最高优先级): 根据你提供的 DOM 结构精准查找
    // 查找 <p class="YtmCommentRendererText"> 或 .user-text
    el = comment.querySelector('.YtmCommentRendererText');
    if (!el) el = comment.querySelector('.user-text');

    // 策略 B: 如果上面的没找到,尝试查找 content 容器内的 span
    // 排除 header (避免抓到时间戳)
    if (!el) {
      const contentSection = comment.querySelector('.comment-content');
      if (contentSection) {
        el = contentSection.querySelector('.yt-core-attributed-string');
      }
    }

    // 策略 C: 最后的保底,但在 .comment-header 之外查找
    if (!el) {
        const allSpans = comment.querySelectorAll('.yt-core-attributed-string[role="text"]');
        for (const span of allSpans) {
            // 确保这个 span 不是时间戳 (通常时间戳在 header 里)
            if (!span.closest('.comment-header') && !span.closest('.YtmCommentRendererTitle')) {
                el = span;
                break;
            }
        }
    }

    if (!el) return null;

    // 清理文本
    let txt = el.textContent.trim().replace(/\s+/g, ' ');
    // 截断长文本 (保留前40字)
    if (txt.length > 40) txt = txt.slice(0, 40) + '…';
    return txt;
  }

  // ====== 4. 全局点击监听 ======
  document.addEventListener('click', function(e) {
    // 向上查找评论容器 (兼容新旧两种容器名)
    const comment = e.target.closest('ytm-comment-renderer') || e.target.closest('.comment-view-model');
    
    if (comment) {
      const user = extractUsername(comment);
      const text = extractCommentText(comment); // 这里调用修复后的函数
      
      if (user) lastClickedUser = user;
      if (text) lastClickedText = text;
      
      // 调试用:如果不确定抓到了什么,可以取消下面这行的注释
      // console.log('抓取测试:', user, text);
    }

    // 检测是否点击了回复按钮
    // 兼容中文、英文、图标按钮
    const targetText = e.target.textContent?.trim();
    const btn = e.target.closest('button') || e.target;
    const isReplyBtn = targetText === 'Reply' || targetText === '回复' || 
                       btn.getAttribute('aria-label') === 'Reply' ||
                       btn.getAttribute('aria-label') === '回复';

    if (isReplyBtn) {
      if (lastClickedUser) {
        showDebugMsg(`准备回复: ${lastClickedUser}`);
        waitForReplyDialog();
      }
    }
  }, true);

  // ====== 5. 等待输入框出现 ======
  function waitForReplyDialog() {
    let attempts = 0;
    const interval = setInterval(() => {
      attempts++;
      // 查找输入框 (多重选择器兼容)
      const textarea = document.querySelector('textarea.YtmCommentReplyDialogRendererInput, textarea[placeholder*="reply"], textarea[placeholder*="回复"]');
      
      if (textarea) {
        clearInterval(interval);
        insertContent(textarea);
      }
      if (attempts > 20) clearInterval(interval); // 2秒超时
    }, 100);
  }

  // ====== 6. 插入内容 ======
  function insertContent(textarea) {
    if (!textarea || !lastClickedUser) return;

    let username = lastClickedUser.trim();
    if (!username.startsWith('@')) username = '@' + username;

    let finalContent = `${username} `;
    
    // 根据开关判断是否插入引用
    if (isQuoteEnabled && lastClickedText) {
      // 检查抓到的文本是否像是时间戳 (例如包含 "ago", "前", 数字+小时 等)
      // 这是一个额外的保险措施
      const isTimeLike = /^\d+\s?(小时|天|周|月|年|minute|hour|day|week|month|year)/.test(lastClickedText);
      
      if (!isTimeLike) {
        finalContent = `${username} 「${lastClickedText}」 `;
      }
    }

    // 使用原生 setter 绕过 React/框架 的输入绑定限制
    const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
    nativeInputValueSetter.call(textarea, finalContent);

    // 触发 input 事件让网页知道有内容输入了
    textarea.dispatchEvent(new Event('input', { bubbles: true }));
    
    // 聚焦并把光标移到最后
    textarea.focus();
    textarea.setSelectionRange(finalContent.length, finalContent.length);
  }

})();