起因
作为一个喜欢到处逛博客、在别人文章下面留言的人,我每隔几天就会遇到同一个烦人场景:
打开一篇不错的文章 → 拉到评论区 → 手动输入昵称 → 手动输入邮箱 → 手动输入网址 → 结果发现这篇文章是 Twikoo、那篇是 Artitalk、另一篇又是 WordPress 原生评论……输入框长得不一样,但我要填的东西一模一样。
更要命的是,很多博客为了防止刷评论,不会自动保存你的信息。你今天填了,明天换个浏览器或者清一下缓存,又得重新填一遍。
填一次两次还行,填个几十次就真的想砸键盘了。
所以我又写了个脚本
成品长这样:页面右下角会出现一个紫色的悬浮按钮「📝 填充」,点一下,评论区的昵称、邮箱、网址就全自动填好了。

使用流程极其简单:
- 点油猴菜单里的「⚙️ 设置留言信息」(或者右键悬浮按钮)
- 填一次你的昵称、邮箱、网址、地址
- 保存
- 以后在任何博客的评论区,只要点一下按钮就行
信息是用 GM_setValue 存在油猴里的,跨站点、跨标签页、跨会话,只要油猴在,你的配置就在。
技术上的几个小坑
写这个脚本看着简单,其实踩了几个坑,记录一下给后来人参考。
坑 1:评论区是懒加载的
很多博客(尤其是 Twikoo、Artitalk 这种基于 Vue 的评论系统)的评论框不是页面加载时就渲染出来的,而是用户滚动到评论区附近或者点击评论框时才动态渲染。
所以脚本不能一上来就去找输入框,得先等页面稳定下来。我现在的做法是页面加载完延迟 1 秒再注入按钮,然后每次点击时实时查询输入框,而不是缓存 DOM 引用。
坑 2:直接改 input.value 没用
这是最坑的一个。对于 Vue / React 这类框架,input.value = 'xxx' 只是改了 DOM 节点的属性,框架内部的响应式数据不会更新。结果就是:输入框看起来填上了,但你一点"发送"按钮,提交的还是空值。
正确做法是模拟真实的输入过程:
// 用原生 setter 赋值(绕过框架对 value 的劫持)
const setter = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype, 'value'
).set;
setter.call(input, value);
// 触发框架监听的事件
input.dispatchEvent(new InputEvent('input', { bubbles: true, data: value }));
input.dispatchEvent(new Event('change', { bubbles: true }));尤其是 InputEvent(比普通的 Event 多带 data 和 inputType),Vue 的 v-model 很多情况下只认它。
坑 3:蜜罐字段
有些博客的表单里藏了一个 input[name="url2"] 这种字段,隐藏起来不让真人看到,专门用来钓机器人的——只要这个字段被填了,就判定是爬虫,评论直接被拒。
所以脚本必须跳过这类字段。我目前是通过名字黑名单 + tabindex="-1" && aria-hidden="true" 的组合来识别。
坑 4:不同评论系统的字段名不一样
这是我遇到的最头疼的问题:
| 评论系统 | 昵称 | 邮箱 | 网址 |
|---|---|---|---|
| Twikoo | nick | mail | link |
| WordPress 原生 | author | email | url |
| 你博客那个自定义表单 | name | email | website |
| Artitalk | nickname | email | link |
所以我的策略是给每个字段配一组选择器,按优先级从上到下试,找到第一个可见且不是蜜罐的就填:
nickname: [
'#twikoo input[name="nick"]',
'input[name="nick"]',
'input[name="nickname"]',
'form.cmt-form input[name="name"]',
'input[name="author"]',
// ...
]目前这套配置覆盖了 Twikoo、WordPress、Typecho、Hexo 常见主题,以及我遇到的几个自定义表单。如果哪天遇到不在列表里的,加一行就行。
关于样式
现在的 UI 就是下面这个样子,紫色渐变,圆角,能拖拽,会实时反馈状态:
- 📝 待机
- ✅ 已填充:昵称、邮箱
- ⚠️ 请先填写配置
- ❌ 未找到输入框
说实话这个配色和交互是我自己随便糊的,能用但不一定好看。
如果你不喜欢这个样式,完全可以把脚本丢给 AI(比如 ChatGPT、Claude、DeepSeek 都行),告诉它"帮我把这个按钮改成 XXX 风格",让它帮你改 CSS。 脚本里所有样式都集中在 GM_addStyle 那一大段,改起来没什么技术门槛。
你也可以让它把按钮换成图标、改成黑色、去掉渐变、变小一点、挪到左下角……只要你能描述出想要的样子,AI 基本都能给你改出来。
完整代码
直接复制到油猴里新建脚本就能用:
// ==UserScript==
// @name 博客留言自动填充
// @namespace http://tampermonkey.net/
// @version 2.2
// @description 在博客留言区自动填充昵称、邮箱、网址、地址
// @author xiangmingya!
// @match *://*/*
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// ==/UserScript==
(function() {
'use strict';
// ============ 配置 ============
const CONFIG_KEY = 'blogAutofillConfigV2';
const defaultConfig = {
nickname: '',
email: '',
website: '',
address: ''
};
// 每个字段一组选择器,按优先级从上到下尝试
const SELECTORS = {
nickname: [
'#twikoo input[name="nick"]',
'.tk-meta-input input[name="nick"]',
'input[name="nick"]',
'input[name="nickname"]',
'form.cmt-form input[name="name"]',
'input[name="author"]',
'input[name="name"]',
'input[name="username"]',
'input[id="author"]',
'input[id="nickname"]',
'input[id="name"]',
'input[autocomplete="nickname"]',
'input[placeholder*="昵称"]',
'input[placeholder*="姓名"]',
'input[placeholder*="名字"]'
],
email: [
'#twikoo input[name="mail"]',
'.tk-meta-input input[name="mail"]',
'input[name="mail"]',
'form.cmt-form input[name="email"]',
'input[name="email"]',
'input[type="email"]',
'input[id="email"]',
'input[id="mail"]',
'input[autocomplete="email"]',
'input[placeholder*="邮箱"]',
'input[placeholder*="邮件"]'
],
website: [
'#twikoo input[name="link"]',
'.tk-meta-input input[name="link"]',
'input[name="link"]',
'form.cmt-form input[name="website"]',
'input[name="url"]',
'input[name="website"]',
'input[name="site"]',
'input[id="url"]',
'input[id="website"]',
'input[autocomplete="url"]',
'input[placeholder*="网址"]',
'input[placeholder*="网站"]'
],
address: [
'input[name="address"]',
'input[id="address"]',
'input[placeholder*="地址"]'
]
};
const HONEYPOT_NAMES = ['url2', 'hp', 'honeypot', 'website_url2', '_gotcha'];
// ============ 配置读写 ============
function loadConfig() {
try {
const raw = GM_getValue(CONFIG_KEY, '');
if (raw) return Object.assign({}, defaultConfig, JSON.parse(raw));
} catch (e) {}
return Object.assign({}, defaultConfig);
}
function saveConfig(cfg) {
GM_setValue(CONFIG_KEY, JSON.stringify(cfg));
}
// ============ DOM 查找 ============
function isHoneypot(el) {
const name = (el.getAttribute('name') || '').toLowerCase();
if (HONEYPOT_NAMES.includes(name)) return true;
if (el.getAttribute('tabindex') === '-1' && el.getAttribute('aria-hidden') === 'true') return true;
if (el.classList && el.classList.contains('cmt-hp')) return true;
return false;
}
function isVisible(el) {
if (el.getClientRects().length === 0) return false;
const style = window.getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden') return false;
return true;
}
function findInput(selectors) {
for (const sel of selectors) {
let list;
try { list = document.querySelectorAll(sel); } catch (e) { continue; }
for (const el of list) {
if (el.disabled || el.readOnly) continue;
if (isHoneypot(el)) continue;
if (!isVisible(el)) continue;
return el;
}
}
return null;
}
// ============ 输入模拟(兼容 Vue/Element UI)============
function simulateInput(el, value) {
el.focus();
el.click();
const proto = el.tagName === 'TEXTAREA'
? window.HTMLTextAreaElement.prototype
: window.HTMLInputElement.prototype;
const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
setter.call(el, '');
el.dispatchEvent(new Event('input', { bubbles: true }));
setter.call(el, value);
// 完整事件链
try {
el.dispatchEvent(new InputEvent('input', {
bubbles: true, cancelable: true, data: value, inputType: 'insertText'
}));
} catch (e) {
el.dispatchEvent(new Event('input', { bubbles: true }));
}
el.dispatchEvent(new Event('change', { bubbles: true }));
try {
el.dispatchEvent(new CompositionEvent('compositionend', { bubbles: true, data: value }));
} catch (e) {}
el.dispatchEvent(new Event('input', { bubbles: true }));
// 触发 Vue 更新(如可用)
if (el._vModifiers || el.__vue__) {
try {
const evt = document.createEvent('HTMLEvents');
evt.initEvent('input', true, true);
el.dispatchEvent(evt);
} catch (e) {}
}
el.dispatchEvent(new Event('blur', { bubbles: true }));
}
// ============ 填充主逻辑 ============
function fillForm() {
const cfg = loadConfig();
const details = [];
const skipped = [];
const fields = [
{ key: 'nickname', val: cfg.nickname, label: '昵称' },
{ key: 'email', val: cfg.email, label: '邮箱' },
{ key: 'website', val: cfg.website, label: '网址' },
{ key: 'address', val: cfg.address, label: '地址' }
];
// 检查是否有任何已配置的值
const hasAnyValue = fields.some(f => f.val);
console.log('=== 开始填充 ===');
if (!hasAnyValue) {
console.warn('⚠️ 没有填写任何配置信息');
return { count: 0, details, skipped, noConfig: true };
}
for (const f of fields) {
if (!f.val) { continue; }
const el = findInput(SELECTORS[f.key]);
if (el) {
simulateInput(el, f.val);
details.push(f.label);
console.log(`✅ ${f.label} →`, el, '当前值:', el.value);
} else {
skipped.push(f.label);
console.log(`❌ ${f.label} 未找到`);
}
}
console.log('=== 填充结束,成功:', details, '跳过:', skipped, '===');
return { count: details.length, details, skipped, noConfig: false };
}
// ============ 调试 ============
function debugDump() {
console.log('=== 自动填充调试信息 ===');
document.querySelectorAll('input, textarea').forEach((el, i) => {
console.log(`[${i}]`, {
tag: el.tagName, type: el.type,
name: el.name, id: el.id, placeholder: el.placeholder,
disabled: el.disabled, readOnly: el.readOnly,
visible: isVisible(el),
honeypot: isHoneypot(el),
value: el.value
});
});
console.log('=== 调试结束 ===');
}
GM_registerMenuCommand('🔍 调试:打印所有输入框', debugDump);
// ============ 悬浮按钮 ============
function createFloatingButton() {
if (document.getElementById('blog-autofill-btn')) return;
const btn = document.createElement('div');
btn.id = 'blog-autofill-btn';
btn.textContent = '📝 填充';
btn.title = '点击自动填充(右键设置)';
GM_addStyle(`
#blog-autofill-btn {
position: fixed; right: 20px; bottom: 100px; z-index: 999999;
background: linear-gradient(135deg, #667eea, #764ba2);
color: #fff; padding: 10px 16px; border-radius: 24px;
cursor: pointer; font-size: 14px; font-weight: 700;
box-shadow: 0 4px 15px rgba(102,126,234,.4);
transition: all .3s; user-select: none;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
#blog-autofill-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(102,126,234,.6); }
#blog-autofill-btn.success { background: linear-gradient(135deg, #11998e, #38ef7d); }
#blog-autofill-btn.error { background: linear-gradient(135deg, #eb3349, #f45c43); }
#blog-autofill-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,.5);
z-index: 999999; display: none;
}
#blog-autofill-overlay.show { display: block; }
#blog-autofill-panel {
position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%);
z-index: 1000000; background: #fff; border-radius: 12px;
box-shadow: 0 20px 60px rgba(0,0,0,.3); padding: 24px;
width: 90%; max-width: 460px; display: none; color: #333;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
#blog-autofill-panel.show { display: block; }
.baf-title { font-size: 18px; font-weight: 700; margin-bottom: 20px; }
.baf-field { margin-bottom: 14px; }
.baf-field label { display: block; font-size: 13px; color: #666; margin-bottom: 6px; }
.baf-field input {
width: 100%; padding: 10px 12px; border: 1px solid #ddd;
border-radius: 8px; font-size: 14px; box-sizing: border-box;
outline: none; font-family: inherit;
}
.baf-field input:focus { border-color: #667eea; box-shadow: 0 0 0 3px rgba(102,126,234,.15); }
.baf-buttons { display: flex; gap: 10px; margin-top: 20px; }
.baf-buttons button {
flex: 1; padding: 10px; border: none; border-radius: 8px;
font-size: 14px; font-weight: 600; cursor: pointer;
}
.baf-save { background: linear-gradient(135deg, #667eea, #764ba2); color: #fff; }
.baf-cancel { background: #f0f0f0; color: #666; }
.baf-tip { font-size: 12px; color: #999; margin-top: 12px; line-height: 1.5; }
`);
document.body.appendChild(btn);
let isDragging = false;
btn.addEventListener('click', function() {
if (isDragging) { isDragging = false; return; }
const r = fillForm();
// 情况1:没配置任何信息 → 自动打开设置面板
if (r.noConfig) {
btn.classList.add('error');
btn.textContent = '⚠️ 请先填写配置';
setTimeout(() => {
btn.classList.remove('error');
btn.textContent = '📝 填充';
openPanel();
}, 600);
return;
}
// 情况2:有配置且填充成功
if (r.count > 0) {
btn.classList.add('success');
let msg = `✅ 已填充:${r.details.join('、')}`;
if (r.skipped.length > 0) {
msg += `(跳过 ${r.skipped.join('、')})`;
}
btn.textContent = msg;
setTimeout(() => {
btn.classList.remove('success');
btn.textContent = '📝 填充';
}, 2500);
return;
}
// 情况3:有配置但页面没找到输入框
btn.classList.add('error');
btn.textContent = '❌ 未找到输入框(F12 看日志)';
setTimeout(() => {
btn.classList.remove('error');
btn.textContent = '📝 填充';
}, 2500);
});
btn.addEventListener('contextmenu', e => { e.preventDefault(); openPanel(); });
let sx, sy, sl, st;
btn.addEventListener('mousedown', e => {
if (e.button !== 0) return;
isDragging = false;
sx = e.clientX; sy = e.clientY;
const r = btn.getBoundingClientRect();
sl = r.left; st = r.top;
const move = e2 => {
const dx = e2.clientX - sx, dy = e2.clientY - sy;
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) {
isDragging = true;
btn.style.right = 'auto'; btn.style.bottom = 'auto';
btn.style.left = (sl + dx) + 'px';
btn.style.top = (st + dy) + 'px';
}
};
const up = () => {
document.removeEventListener('mousemove', move);
document.removeEventListener('mouseup', up);
};
document.addEventListener('mousemove', move);
document.addEventListener('mouseup', up);
});
}
// ============ 设置面板 ============
function openPanel() {
const cfg = loadConfig();
let ov = document.getElementById('blog-autofill-overlay');
let pn = document.getElementById('blog-autofill-panel');
if (!ov) { ov = document.createElement('div'); ov.id = 'blog-autofill-overlay'; document.body.appendChild(ov); }
if (!pn) { pn = document.createElement('div'); pn.id = 'blog-autofill-panel'; document.body.appendChild(pn); }
pn.innerHTML = `
<div class="baf-title">⚙️ 留言信息设置</div>
<div class="baf-field"><label>昵称</label>
<input type="text" id="baf-nickname" value="${esc(cfg.nickname)}" placeholder="你的昵称"></div>
<div class="baf-field"><label>邮箱</label>
<input type="email" id="baf-email" value="${esc(cfg.email)}" placeholder="你的邮箱"></div>
<div class="baf-field"><label>网址(可选)</label>
<input type="url" id="baf-website" value="${esc(cfg.website)}" placeholder="https://example.com"></div>
<div class="baf-field"><label>地址(可选)</label>
<input type="text" id="baf-address" value="${esc(cfg.address)}" placeholder="你的地址"></div>
<div class="baf-buttons">
<button class="baf-cancel" id="baf-cancel">取消</button>
<button class="baf-save" id="baf-save">保存</button>
</div>
<div class="baf-tip">💡 保存后点击右下角"填充"按钮即可。右键按钮可再次打开设置。</div>
`;
ov.classList.add('show');
pn.classList.add('show');
// 空字段标红提示
setTimeout(() => {
['baf-nickname', 'baf-email'].forEach(id => {
const el = document.getElementById(id);
if (el && !el.value.trim()) {
el.style.borderColor = '#eb3349';
el.placeholder = '⚠️ 建议填写';
}
});
}, 50);
const close = () => { ov.classList.remove('show'); pn.classList.remove('show'); };
ov.onclick = close;
document.getElementById('baf-cancel').onclick = close;
document.getElementById('baf-save').onclick = function() {
const c = loadConfig();
c.nickname = document.getElementById('baf-nickname').value.trim();
c.email = document.getElementById('baf-email').value.trim();
c.website = document.getElementById('baf-website').value.trim();
c.address = document.getElementById('baf-address').value.trim();
saveConfig(c);
this.textContent = '✅ 已保存';
setTimeout(() => { this.textContent = '保存'; close(); }, 800);
};
}
function esc(s) {
if (!s) return '';
return s.replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
}
GM_registerMenuCommand('⚙️ 设置留言信息', openPanel);
// ============ 启动 ============
function init() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => setTimeout(createFloatingButton, 1000));
} else {
setTimeout(createFloatingButton, 1000);
}
}
init();
})();💡 首次使用记得先在设置里填一次自己的信息,否则点填充会提示你先配置。
一点碎碎念
写这种小工具最大的乐趣在于:解决的是自己每天都会遇到的小麻烦,但解决之后每次用都会爽一下。
我知道网上已经有类似的脚本了(比如「自动填充评论」之类的),但很多都只支持特定的评论系统,或者要改一堆配置。我这个的思路是通用优先——用一组兜底选择器尽量覆盖大多数博客,同时把蜜罐、框架兼容这些细节处理好。
如果你也在到处逛博客、到处留言,希望这个脚本能帮你省下每次输入昵称邮箱的那几十秒。积少成多,一年下来也是好几个小时呢(认真脸)。
反馈:有问题欢迎在评论区留言,或者直接丢给 AI 帮你改
对了,如果哪天你发现某个博客的评论区填不进去,可以按 F12 看控制台,脚本会打印它尝试过哪些选择器。把日志发给我,我加一行配置就行。
