【中/英】在 YouTube 手机版点击“回复”时自动在输入框插入 @用户名。Automatically insert @username when replying to comments on YouTube Mobile.
// ==UserScript==
// @name YouTube Mobile 评论自动@用户名 / Auto @username in YouTube Mobile Replies
// @namespace yt-mobile-autoreply
// @version 1.0
// @description 【中/英】在 YouTube 手机版点击“回复”时自动在输入框插入 @用户名。Automatically insert @username when replying to comments on YouTube Mobile.
// @author Kemcy BEST & ChatGPT
// @match https://m.youtube.com/*
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// ==== Detect language ====
const lang = (navigator.language || navigator.userLanguage || 'en').toLowerCase().startsWith('zh') ? 'zh' : 'en';
// ==== i18n strings ====
const text = {
zh: {
loaded: '[YT 自动回复脚本] 已加载',
capture: '[YT 自动回复脚本] 捕获用户名: ',
clickReply: '[YT 自动回复脚本] 检测到点击 Reply 按钮',
shadowReply: '[YT 自动回复脚本] Shadow DOM 内检测到 Reply 点击',
dialogFound: '[YT 自动回复脚本] 回复输入框已出现',
success: '[YT 自动回复脚本] 插入成功 ✅ ',
},
en: {
loaded: '[YT AutoReply] Script loaded',
capture: '[YT AutoReply] Captured username: ',
clickReply: '[YT AutoReply] Detected Reply button click',
shadowReply: '[YT AutoReply] Detected Reply click inside Shadow DOM',
dialogFound: '[YT AutoReply] Reply input detected',
success: '[YT AutoReply] Insert success ✅ ',
}
}[lang];
// ====== 页面日志(3秒后自动消失) ======
function log(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: '0',
left: '0',
background: 'rgba(0,0,0,0.75)',
color: '#0f0',
fontSize: '11px',
padding: '4px 6px',
zIndex: 999999,
fontFamily: 'monospace',
pointerEvents: 'none'
});
document.body.appendChild(box);
}
const line = document.createElement('div');
line.textContent = msg;
line.style.transition = 'opacity 0.6s ease';
box.appendChild(line);
setTimeout(() => {
line.style.opacity = '0';
setTimeout(() => line.remove(), 600);
}, 3000);
}
let lastClickedUser = null;
// ====== 抓用户名,多 selector 兜底 ======
function extractUsername(comment) {
if (!comment) return null;
const sel = [
'.YtmCommentRendererTitle .yt-core-attributed-string',
'a.yt-core-attributed-string',
'yt-attributed-string',
'[id*="author-text"]',
'[role="link"] span'
];
for (const s of sel) {
const el = comment.querySelector(s);
if (el && el.textContent.trim()) return el.textContent.trim();
}
return null;
}
// ====== 全局点击捕获 ======
document.addEventListener('click', function(e) {
const comment = e.target.closest('ytm-comment-renderer');
if (comment) {
lastClickedUser = extractUsername(comment);
log(`${text.capture}${lastClickedUser || '(null)'}`);
}
const replyText = e.target.textContent?.trim();
if (replyText === 'Reply' || replyText === '回复') {
log(text.clickReply);
waitForReplyDialog();
}
}, true);
// ====== Shadow DOM 支持 ======
const openShadow = Element.prototype.attachShadow;
Element.prototype.attachShadow = function(init) {
const shadow = openShadow.call(this, init);
shadow.addEventListener('click', function(ev) {
const textContent = ev.target.textContent?.trim();
if (textContent === 'Reply' || textContent === '回复') {
log(text.shadowReply);
waitForReplyDialog();
}
}, true);
return shadow;
};
// ====== 等待对话框出现 ======
function waitForReplyDialog() {
const observer = new MutationObserver(() => {
const textarea = document.querySelector('dialog .YtmCommentReplyDialogRendererInput');
if (textarea) {
log(text.dialogFound);
observer.disconnect();
insertUsernameWithRetry(textarea);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
// ====== 稳定插入逻辑 ======
function insertUsernameWithRetry(textarea, attempt = 0) {
if (!textarea || !lastClickedUser) return;
const username = lastClickedUser.startsWith('@') ? lastClickedUser : '@' + lastClickedUser;
setTimeout(() => {
textarea.focus();
textarea.value = `${username} `;
textarea.dispatchEvent(new Event('input', { bubbles: true }));
if (textarea.value.startsWith(username)) {
log(`${text.success}${username}`);
} else if (attempt < 15) {
insertUsernameWithRetry(textarea, attempt + 1);
}
}, 100 + attempt * 100);
}
log(text.loaded);
})();