抖音聊天监控与自动回复

抖音聊天监控与自动回复

前言

抖音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,重新启动

使用方法

  1. 打开抖音PC端,进入群聊页面
  2. 修改 autoReply 数组配置关键词和回复
  3. 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插件,一键启用