// ==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 // @downloadURL none // ==/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 结构精准查找 // 查找

或 .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); } })();