Greasy Fork is available in English.
自动插入“@用户名”,可选择是否引用原评论。修复抓取到时间戳的问题。
当前为
// ==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);
}
})();