前言
抖音PC端没有开放聊天API,想做消息监控或自动回复只能自己动手。传统方案是抓包拦截WebSocket,门槛高且容易被检测。本文介绍一种纯前端注入方案,零依赖,浏览器控制台粘贴即用。
核心思路
- MutationObserver 监听DOM变化,实时捕获新消息
- data-index="0" 始终指向最新一条消息,避免全量扫描
- data-index递增遍历 查找连续消息的真实发送者昵称
- document.execCommand 向Slate富文本编辑器写入内容并模拟发送
踩坑记录
消息重复输出
[class*="message"] 选择器太宽泛,匹配到了消息容器的子元素,一条消息被处理多次。改用精确类名 .messageMessageBoxmessageBox 解决。
昵称互相覆盖
最初用一个变量 lastNick 记住上一个昵称,但群聊里多个人轮流发言会互相覆盖。拆分为 lastSelfNick / lastOtherNick 后仍然有问题——群聊中任何非己方消息的 isSelf 都是 false,无法区分是A发的还是B发的。
真正解决是用 data-index 向上遍历查找。抖音群聊的消息结构中,同一个人连续发消息时只有第一条显示昵称,后续消息省略头像和昵称元素。当我们需要获取当前消息的发送者昵称时,直接查 .MessageBoxMessageTitleavatarName 元素,如果没有就往 data-index+1 的消息找,直到找到有昵称的为止。
发送后旧消息重复
DOM重新渲染后元素引用失效,Set 去重失败。改用 data-index="0" 只监听最新消息,配合 lastContent 内容对比去重。
两个data-index="0"
左侧会话列表也有 data-index="0",querySelector 会误匹配。遍历所有 data-index="0",只取包含 .messageMessageBoxmessageBox 的那个。
写入输入框但发送按钮禁用
直接改 textContent 不会触发Slate编辑器内部状态更新。改用 document.execCommand('insertText') + Selection API,Slate能正确识别。
连发相同消息被吞
按内容去重导致相同内容被过滤。改为只对比 lastContent,新消息插入后 data-index="0" 的DOM元素变了,自然不会被误判。
昵称元素根本不存在
最初尝试用"有没有头像"来判断是否需要查历史,这是不对的。.commonIMAvataravatarContainer 不存在不代表没有昵称,应该直接查 .MessageBoxMessageTitleavatarName 元素。
功能
- 实时监听:文本、图片、大表情、小表情、分享视频
- 昵称识别:连续消息无头像时自动往上查找真实发送者
- 关键词自动回复:支持精确匹配和模糊匹配,3秒冷却防刷屏
- 重复注入自动清理:断开旧Observer,重新启动
使用方法
- 打开抖音PC端,进入群聊页面
- 修改
autoReply数组配置关键词和回复 - F12打开控制台,粘贴代码回车
// matchType: 'exact' = 精确匹配, 'fuzzy' = 模糊匹配
const autoReply = [
{ keyword: '在吗', reply: '在的~', matchType: 'exact' },
{ keyword: '加群', reply: '群主,群众中有坏蛋', matchType: 'fuzzy' }
];完整代码
(function() {
if (window.__chatMonitorObserver) {
window.__chatMonitorObserver.disconnect();
window.__chatMonitorObserver = null;
console.log('[已移除旧注入,重新启动]');
}
window.__chatMonitorRunning = true;
let lastContent = '';
let lastMsgEl = null;
// matchType: 'exact' = 精确匹配(消息内容完全等于关键词), 'fuzzy' = 模糊匹配(消息内容包含关键词)
const autoReply = [
{ keyword: '建档', reply: '微扫置顶小程序码!', matchType: 'exact' },
{ keyword: '在吗', reply: '在的~', matchType: 'exact' },
{ keyword: '加群', reply: '群主,群众中有坏蛋', matchType: 'fuzzy' }
];
let replyCooldown = false;
function getNick(el) {
const nameEl = el.querySelector('.MessageBoxMessageTitleavatarName');
if (nameEl) return nameEl.textContent ? nameEl.textContent.trim() : '';
return '';
}
function getContent(el) {
const shareBox = el.querySelector('.MessageItemShareAwemecontainer');
if (shareBox) {
const autorName = shareBox.querySelector('.MessageItemShareAwemeautorName');
const coverImg = shareBox.querySelector('img.commonMyImageimgReal');
var result = '[分享]';
if (autorName) result += ' @' + autorName.textContent.trim();
if (coverImg && coverImg.src) result += ' ' + coverImg.src;
return result;
}
var imgBox = el.querySelector('.MessageItemImageImageBox');
if (imgBox) {
var img = imgBox.querySelector('img');
if (img && img.src) return '[图片] ' + img.src;
return '[图片]';
}
var emojiBox = el.querySelector('.MessageItemEmojiemojiBox');
if (emojiBox) {
var emojiImg = emojiBox.querySelector('img');
if (emojiImg && emojiImg.src) return '[大表情] ' + emojiImg.src;
return '[大表情]';
}
var textEl = el.querySelector('.TextMessageTextpureText');
if (textEl) return textEl.textContent ? textEl.textContent.trim() : '';
var emojiEl = el.querySelector('.TextMessageTextemoji img');
if (emojiEl) {
var title = emojiEl.getAttribute('title');
var src = emojiEl.src;
if (title && src) return title + ' ' + src;
if (title) return title;
if (src) return '[表情] ' + src;
return '[表情]';
}
return '';
}
function sendMessage(text) {
var editor = document.querySelector('[data-slate-editor="true"]');
if (!editor) {
console.log('[自动回复] 未找到输入框');
return;
}
editor.focus();
setTimeout(function() {
var sel = window.getSelection();
var range = document.createRange();
range.selectNodeContents(editor);
sel.removeAllRanges();
sel.addRange(range);
document.execCommand('insertText', false, text);
setTimeout(function() {
var sendBtn = document.querySelector('.e2e-send-msg-btn');
if (sendBtn) {
sendBtn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
console.log('[自动回复] 已发送: ' + text);
} else {
console.log('[自动回复] 未找到发送按钮');
}
}, 800);
}, 300);
}
function checkAutoReply(content, isSelf) {
if (isSelf || replyCooldown) return;
for (var i = 0; i < autoReply.length; i++) {
var rule = autoReply[i];
var matched = false;
if (rule.matchType === 'exact') {
matched = (content === rule.keyword);
} else {
matched = (content.indexOf(rule.keyword) !== -1);
}
if (matched) {
replyCooldown = true;
setTimeout(function() { replyCooldown = false; }, 3000);
setTimeout(function() { sendMessage(rule.reply); }, 500);
return;
}
}
}
function findChatIndex0() {
var all = document.querySelectorAll('[data-index="0"]');
for (var i = 0; i < all.length; i++) {
if (all[i].querySelector('.messageMessageBoxmessageBox')) return all[i];
}
return null;
}
function findNickFromHistory(targetMsgBox) {
var container = targetMsgBox.parentElement;
while (container && !container.querySelector('[data-index="0"]')) {
container = container.parentElement;
}
if (!container) return '';
var currentIndex = parseInt(targetMsgBox.getAttribute('data-index') || '0', 10);
var searchIdx = currentIndex + 1;
var maxSearch = 10000;
while (searchIdx < maxSearch) {
var prevIndexEl = container.querySelector('[data-index="' + searchIdx + '"]');
if (prevIndexEl) {
var prevMsgBox = prevIndexEl.querySelector('.messageMessageBoxmessageBox');
if (prevMsgBox) {
var nick = getNick(prevMsgBox);
if (nick) return nick;
}
}
searchIdx++;
}
return '';
}
function checkLatest() {
var indexEl = findChatIndex0();
if (!indexEl) return;
var msgBox = indexEl.querySelector('.messageMessageBoxmessageBox');
if (!msgBox) return;
if (msgBox.querySelector('.MessageBoxRecalledrecallLayout')) return;
var contentBox = msgBox.querySelector('.messageMessageBoxcontentBox');
if (!contentBox) return;
var content = getContent(msgBox);
if (!content) return;
if (content === '已读' || content === '发送中') return;
if (content === lastContent && msgBox === lastMsgEl) return;
lastContent = content;
lastMsgEl = msgBox;
var isSelf = contentBox.classList.contains('messageMessageBoxisFromMe');
var nick = getNick(msgBox);
if (!nick) nick = findNickFromHistory(msgBox);
if (!nick) nick = isSelf ? '我' : '对方';
var action = isSelf ? '发送' : '收到';
console.log('[' + action + '] 昵称:' + nick + ' 消息:' + content);
checkAutoReply(content, isSelf);
}
var observer = new MutationObserver(function() {
setTimeout(checkLatest, 200);
});
window.__chatMonitorObserver = observer;
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(checkLatest, 500);
console.log('[监测已启动] 软件/网站/小程序开发,JS逆向,微信: Akaxz_');
var rules = autoReply.map(function(r) { return '"' + r.keyword + '"→"' + r.reply + '"'; }).join(', ');
console.log('[自动回复规则] ' + rules);
})();扩展方向
- 对接后端API,将消息推送到服务器
- 接入AI接口实现智能回复
- 添加更多消息类型支持(语音、视频通话等)
- 打包成Chrome插件,一键启用