Douyin Messager | 抖音私信助手

Other

Douyin DMs and video/note comment assistant. 抖音私信与视频/图文评论助手;可读私信、分析评论区,评论/回复等写入前必须确认。

Install

openclaw skills install douyin-messager

Douyin Messager | 抖音私信助手

通过浏览器自动化发送抖音私信、获取聊天记录;也支持打开抖音视频/图文链接、读取评论区并做简要分析,在用户明确确认后执行评论/回复等写入操作。

前置要求 | Prerequisites

条件说明
Browser profile必须使用 openclaw profile
登录状态抖音账号已在 openclaw profile 中保持登录态

⚠️ 执行前必须确认

  1. 用户已登录抖音账号(可目测判断)
  2. xdg-open 弹窗:只在 Linux 下存在
    • Linux:询问用户弹窗是否关闭
    • Windows/macOS:跳过

⚠️ 安全声明 | Security Notice

本技能通过已登录的抖音浏览器会话读写私信和评论。

浏览器会话凭据声明

本技能依赖已登录的抖音浏览器会话作为账户凭据。使用前请确认:

  1. 确认登录账户:发送操作前,先确认当前登录的是正确的抖音账户
  2. 使用专用 profile:必须使用独立的 openclaw profile,不与日常浏览器混用
  3. 用完后清理:关闭 openclaw profile 的浏览器标签页即可;不再使用时,可清除该 profile 的抖音登录态

数据披露

  • 读取聊天记录和评论区会将内容暴露到 agent 上下文,仅读取你愿意分享的内容
  • 不要用于读取高度敏感或非自愿的私人对话

操作限制

  • 发送私信:必须先向用户确认目标账号和消息内容,获得明确同意后才执行
  • 评论/回复/点赞/分享:属于外部互动写入操作,必须先获得用户明确确认

当前能力边界(阶段性总结)

本技能当前已验证可完成四类操作:

  1. 查看私信会话列表:进入私信悬浮面板后,读取可见的私聊/群聊列表,区分会话名称、最新消息预览、时间、未读数和置顶状态。
  2. 进入具体聊天窗口:从会话列表点击指定私聊或群聊,进入聊天详情页,读取当前已加载的聊天记录。
  3. 发送文本消息:在聊天详情底部的 Draft.js 输入框中输入文本,并通过发送按钮完成发送。
  4. 视频/图文评论区处理:可搜索或打开指定视频/图文链接,读取评论区,输出情绪简报;评论、回复、点赞等外部互动写入必须先获得用户明确确认。

需要注意:网页版对视频、图集、点赞、撤回、暂不支持消息等卡片类内容的 DOM 暴露不完整,读取时应保守标注,不应强行推断完整内容或发送者。


核心思路

抖音私信是一个悬浮在页面右侧的动态面板,包含"会话列表"和"聊天详情"两个视图。

会话列表布局(类 QQ/微信):左侧头像,中间分上下两行(上行:用户名/群名;下行:最新消息预览,群聊格式为「发消息的人:消息内容」),右侧时间戳。

聊天详情布局:进入某个会话后,右侧显示聊天记录。普通文本消息通常可提取时间、消息内容,并可根据气泡方向、操作项(如是否有「撤回」)辅助判断是否为本人消息;群聊中他人文本消息有时可从消息文本或引用结构中识别发送者。卡片类消息需谨慎处理。

方法论:不依赖固定 class name,而是通过元素的几何特征 + 内容特征动态查找。具体 class name 仅作参考示例。


获取私信列表流程

获取私信的第一步不是进入某个聊天,而是正确进入私信会话列表

步骤 1:打开私信会话列表

  1. 打开抖音主页或个人主页。
  2. 在顶部导航栏右侧定位「私信」入口(可能显示未读数,如「私信11」)。
  3. 点击后等待右侧悬浮面板出现。
  4. 确认当前处于会话列表视图,而不是某个聊天详情页。

步骤 2:识别每一行会话

会话列表中每一行对应一个私聊或群聊。布局通常为:左侧头像;中间上行为会话名称(用户名/群名,可能带「置顶」标记);中间下行为最新消息预览;右侧为时间戳或未读标记。

⚠️ 会话列表本身是可滚动容器,不要只按当前可见区域判断数量。实测中滚动容器宽约 328px、高约 593px,scrollHeight > clientHeight,可通过 scrollTop 滚到底部;出现「暂时没有更多了」才表示到底。

解析时应优先按行容器处理,而不是直接读取父元素的合并文本。读取每行时分别提取:

字段识别方法
会话名称行内上方文本,通常是用户名或群名;若有「置顶」应作为标记处理,不并入名称
最新消息行内下方文本;群聊常见格式为「发送者:消息内容」
最新发送者若最新消息包含「:」,冒号前通常是群聊中最新消息发送者
时间行右侧或下行末尾的时间文本,如「昨天」「04-25」「01:42」
未读数行右侧 badge 数字;顶部「私信(N)」通常表示总未读数
置顶名称行附近出现「置顶」标记时记录为 pinned=true

步骤 3:区分私聊与群聊

  • 私聊:上行通常是对方昵称;下行多为消息内容本身。
  • 群聊:上行是群名;下行常为「发送者:消息内容」。
  • 不要把上行群名和下行发送者合并为一个名称。

示例参考

上行(会话名称)下行(最新消息)解析结果
示例群A成员甲:分享[视频] · 昨天群名/会话名=示例群A,最新发送者=成员甲
项目交流群成员乙:分享[图集] · 04-25群名=项目交流群,最新发送者=成员乙
联系人A + 置顶收到,谢谢 · 01:42私聊=联系人A,pinned=true

示例:提取可见会话行文本

以下代码用于辅助观察当前可见列表,不应把 class name 当成稳定依赖:

browser action=act request={"kind": "evaluate", "fn": "() => { const rows = []; const all = Array.from(document.querySelectorAll('*')); for (const el of all) { const r = el.getBoundingClientRect(); const text = (el.textContent || '').trim(); if (r.width >= 250 && r.width <= 360 && r.height >= 50 && r.height <= 90 && r.left > window.innerWidth * 0.55 && text && !text.includes('私信(')) { rows.push({top: Math.round(r.top), text}); } } return rows.sort((a,b) => a.top - b.top); }"}

完整发送流程

步骤 1:打开抖音主页

browser action=open profile=openclaw targetUrl=https://www.douyin.com/
browser action=act request={"kind": "wait", "timeMs": 3000}

步骤 2:打开私信悬浮面板

找按钮:在页面顶部导航栏中,找到文字为"私信"(可能带未读数如"私信15")且位于页面最右侧的元素,点击它。

browser action=act request={"kind": "evaluate", "fn": "() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const t = (el.textContent||'').trim(); const r = el.getBoundingClientRect(); if((t === '私信' || t.startsWith('私信')) && r.width > 0 && r.height > 0 && r.width < 100 && r.left > window.innerWidth - 200) { el.click(); return 'clicked: ' + t; } } return 'not found'; }"}
browser action=act request={"kind": "wait", "timeMs": 2000}

找面板:点击后,页面右侧应出现一个约 500-600px 宽、600-700px 高的悬浮面板。

browser action=act request={"kind": "evaluate", "fn": "() => { const all = document.querySelectorAll('[class]'); for(const el of all) { const r = el.getBoundingClientRect(); if(r.width >= 450 && r.width <= 650 && r.height >= 500 && r.height <= 750 && r.left > 0 && r.top > 0 && r.left < 1500) { const text = (el.textContent||'').trim(); if(text.includes('私信')) return 'found panel: ' + r.width + 'x' + r.height + ' at ' + Math.round(r.left); } } return 'panel not found'; }"}

示例参考:实测中面板容器 class 为 vgonMAXk,宽 581px、高 649px,固定在 x:1235 位置。但这是动态的,应以几何定位为主。

步骤 3:进入具体聊天

找目标用户:在面板的会话列表中,找到目标用户名的元素并点击。

browser action=act request={"kind": "evaluate", "fn": "() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const t = (el.textContent||'').trim(); const r = el.getBoundingClientRect(); if(t.includes('<目标用户名>') && r.width > 0 && r.height > 0 && r.left > 0) { el.click(); return 'clicked: ' + t; } } return 'not found'; }"}
browser action=act request={"kind": "wait", "timeMs": 2000}

成功标志:面板从会话列表切换为聊天详情,底部出现输入区域。

示例参考:会话列表区域 class 为 zEpd_aAP(左侧,500px宽);聊天详情区域 class 为 w5duGc5Q(右侧)。

步骤 4:两步验证(强制,必须执行)

第一步(AI 内部执行,不输出给用户)

  • 确认已正确进入目标会话(检查页面显示对方名字)
  • 确认消息内容已准备完毕
  • 检查消息长度

第二步(输出给用户,等待明确确认)

向用户汇报以下全部内容,确认前不得写入或发送

汇报项内容
目标账号会话对方的名字
消息内容将要发送的完整内容
字数是否超长

确认标志:用户明确回复「好」「确认」「发」「发吧」「发送」等。

步骤 5:写入消息

找输入框:聊天详情底部有一个 Draft.js 富文本编辑器(contenteditable="true",class 包含 DraftEditor-content)。

browser action=act request={"kind": "evaluate", "fn": "() => { const all = document.querySelectorAll('[contenteditable=\"true\"]'); for(const el of all) { const r = el.getBoundingClientRect(); if(r.width > 200 && r.width < 500 && r.height > 10 && r.height < 100 && r.left > 1000) return 'found input at ' + Math.round(r.left) + ',' + Math.round(r.top); } return 'input not found'; }"}

发送消息:先用 type 方式写入 Draft.js 编辑器,再点击发送按钮。

// 1. 写入文本
browser action=act request={"kind": "type", "selector": "[contenteditable=\"true\"]", "text": "你好,测试消息"}
browser action=act request={"kind": "wait", "timeMs": 500}

// 2. 点击发送按钮(优先用几何位置查找,也可用实测 class 作为参考)
browser action=act request={"kind": "evaluate", "fn": "() => { const btn = Array.from(document.querySelectorAll('*')).find(el => { const r = el.getBoundingClientRect(); const cls = String(el.className || ''); return r.width >= 20 && r.width <= 50 && r.height >= 20 && r.height <= 50 && r.left > window.innerWidth * 0.7 && r.top > window.innerHeight * 0.55 && (cls.includes('send') || cls.includes('PygT7Ced')); }); if(!btn) return 'send button not found'; const r = btn.getBoundingClientRect(); btn.click(); return 'clicked send at ' + Math.round(r.left + r.width/2) + ',' + Math.round(r.top + r.height/2); }"}
browser action=act request={"kind": "wait", "timeMs": 2000}

⚠️ 必须用 type 写入 Draft.js 编辑器。直接操作 DOM 文本(textContentexecCommandclipboard)无法触发 Draft.js 内部状态,发送按钮会保持禁用。

示例参考:实测中输入框 class 为 notranslate public-DraftEditor-contentcontenteditable="true"),宽约 329px,位置 x:1346, y:651。

步骤 5:确认发送成功

browser action=act request={"kind": "evaluate", "fn": "() => { const input = document.querySelector('[contenteditable=\"true\"]'); if(!input) return 'input not found'; return input.textContent.length === 0 ? 'sent ✓' : 'not sent: ' + input.textContent; }"}

发送成功后:输入框被自动清空。


定位方法总结

目标定位策略实测示例(参考)
「私信」按钮文字="私信"或"私信数字",位于页面最右侧(r.left > innerWidth - 200文字="私信15",viewport x:1708, y:28
私信面板宽 450-650px、高 500-750px、固定在页面右侧,内容含"私信"class=vgonMAXk,581×649px,x:1235
会话列表面板左侧区域class=zEpd_aAP(宽~81px)
聊天详情面板右侧区域class=w5duGc5Q(宽~500px)
输入框contenteditable="true",宽 200-500px,位于页面右侧class=notranslate public-DraftEditor-content,329×22px
发送按钮输入框右侧;写入文本后点击class 示例:PygT7Ced e2e-send-msg-btn

注意:以上尺寸和位置基于典型 1920px 宽屏幕实测。class name 会随抖音版本变化,几何特征定位是更稳定的方法


读取具体聊天记录

进入某个私聊或群聊后,可以读取聊天详情页中当前已加载的消息块。读取时建议按消息块容器处理,而不是直接读取整个聊天面板的合并文本。

可识别字段

字段说明
时间消息块上方或附近的时间文本,如「刚刚」「2026-04-04 19:07」
消息内容普通文本消息可从气泡文本中提取
是否本人消息可结合气泡位置、是否出现「撤回」操作等特征辅助判断
群聊发送者普通文本或引用消息中可能出现发送者名称;若 DOM 未明确暴露,应标注为未知/推断
卡片类内容视频、图集、点赞、撤回、不支持类型等可能只显示占位文本,不应过度解析

示例:读取当前已加载消息块

browser action=act request={"kind": "evaluate", "fn": "() => { const area = document.querySelector('.IRB0Sra6') || document.querySelector('.z1iI1SFY'); if(!area) return 'message area not found'; const blocks = Array.from(area.querySelectorAll('.mM66nPpS')); return blocks.map(block => { const time = block.querySelector('.mA74174G')?.textContent?.trim() || ''; const text = (block.querySelector('.G3hOMUUp') || block.querySelector('.J3X6BOUb') || block).textContent.trim(); const mine = block.textContent.includes('撤回'); return { time, mine, text }; }); }"}

上述 class name 是实测示例;正式逻辑应优先用消息区域的几何位置和消息块尺寸筛选,再结合文本特征解析。


获取对方回复

滚动到最新消息

browser action=act request={"kind": "evaluate", "fn": "() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const r = el.getBoundingClientRect(); if(r.width >= 400 && r.width <= 600 && r.height >= 300 && r.height <= 800 && r.left > 800) { el.scrollTop = el.scrollHeight; return 'scrolled'; } } return 'not found'; }"}
browser action=act request={"kind": "wait", "timeMs": 1000}

截图确认

browser action=screenshot

视频搜索与评论区操作(阶段性验证)

当前已验证可完成以下只读/可定位操作:

  1. 按关键词搜索视频:可直接打开 https://www.douyin.com/search/<关键词>?type=video,从搜索结果中提取 /video/<id> 链接、标题、作者、时间和互动数字。
  2. 打开指定视频/图文:可直接导航到 https://www.douyin.com/video/<id> 或用户给出的分享链接。图文/笔记类内容可能会规范化跳转为 /note/<id>,个人页弹窗链接中的 modal_id=<id> 也可作为目标内容 ID 使用。
  3. 读取视频信息:可从页面文本中读取标题、作者、发布时间、点赞/评论/收藏/分享等可见数字(具体字段需按页面布局解析)。
  4. 读取评论区:视频页评论区在页面下方,需滚动到「全部评论」区域;可读取可见评论的昵称、内容、时间/地区、点赞数、分享/回复入口,并可继续向下滚动加载更多评论。
  5. 定位发评论输入框:点击「留下你的精彩评论吧」后,会出现 Draft.js 输入框:contenteditable="true" 且 class 包含 public-DraftEditor-content
  6. 定位回复输入框:点击某条评论的「回复」后,会在该评论下方出现 Draft.js 输入框,placeholder 形如「回复@用户名」。
  7. 定位发送按钮:输入文字后,评论框右侧会出现图标按钮区,最右侧圆形上箭头按钮为发送/发布入口。
  8. 定位评论点赞入口:每条评论的操作区包含点赞数、分享、回复;点赞图标/数字区域可通过评论块几何位置定位。
  9. 评论区情绪简报:基于已加载评论文本,按正向/中性/负向/争议或信息不足分类,输出样本量、主要情绪、典型主题和置信度;样本少时必须注明限制。

⚠️ 发评论、回复评论、点赞、分享等都属于外部互动写入操作,必须先获得用户明确确认;测试时可以定位输入框和按钮,但不要擅自提交。

搜索并提取视频链接示例

browser action=navigate profile=openclaw targetUrl="https://www.douyin.com/search/OpenClaw?type=video"
browser action=act request={"kind":"wait","timeMs":3000}
browser action=act request={"kind":"evaluate","fn":"() => Array.from(document.querySelectorAll('a[href*=\"/video/\"]')).map(a => ({ href: a.href, text: (a.innerText || a.textContent || '').trim() })).slice(0, 20)"}

打开视频并滚动到评论区

browser action=navigate profile=openclaw targetUrl="https://www.douyin.com/video/<video_id>"
browser action=act request={"kind":"wait","timeMs":5000}
browser action=act request={"kind":"evaluate","fn":"() => { const el = Array.from(document.querySelectorAll('*')).find(el => (el.innerText || el.textContent || '').includes('全部评论') && el.getBoundingClientRect().y > 100); if (el) { el.scrollIntoView({block:'start'}); return 'scrolled to comments'; } return 'comments not found'; }"}

从个人页弹窗链接打开目标内容

用户可能发送形如 https://www.douyin.com/user/self?...&modal_id=<id>&showTab=like 的链接。此类链接可直接打开;若页面进入个人页弹窗,也可提取 modal_id 后直接尝试:

browser action=navigate profile=openclaw targetUrl="https://www.douyin.com/video/<modal_id>"
browser action=act request={"kind":"wait","timeMs":5000}
// 页面可能自动跳转为 /note/<modal_id>,属于正常情况。

读取可见评论示例

browser action=act request={"kind":"evaluate","fn":"() => Array.from(document.querySelectorAll('*')).map(el => { const r = el.getBoundingClientRect(); const text = (el.innerText || el.textContent || '').trim(); return { r, text, cls: String(el.className || '') }; }).filter(o => o.r.width > 300 && o.r.height > 60 && o.text.includes('回复') && o.text.includes('·')).slice(0, 20).map(o => o.text)"}

图文/笔记页的评论区可能在右侧栏,通过「评论(N)」标签切换;即使初始显示 评论(0),点击后也可能加载真实评论。读取时以当前 DOM 文本为准,并检查是否出现「暂时没有更多评论」。

评论区情绪简报模板

评论区情绪简报:
- 样本量:已加载 N 条评论
- 整体情绪:正向 / 中性 / 负向 / 混合 / 信息不足
- 主要主题:……
- 风险/争议点:……
- 置信度:高 / 中 / 低(说明原因)

评论/回复输入框定位

// 点击「留下你的精彩评论吧」或某条评论的「回复」后:
browser action=act request={"kind":"evaluate","fn":"() => { const input = document.querySelector('[contenteditable=\"true\"].public-DraftEditor-content'); if (!input) return 'input not found'; const r = input.getBoundingClientRect(); return { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height), text: input.innerText }; }"}

常见问题

Q: 点击「私信」按钮没反应? A: 确认点击的是正确的按钮(顶栏最右侧,带"私信"文字)。也可先导航到 /user/self 页面再点击。

Q: 私信面板找不到? A: 确认点击后有等待足够时间(wait 2000ms)。面板宽约 500px、高约 650px,固定在页面右侧。

Q: 输入框找不到? A: 必须先在会话列表中点击一个具体用户,进入聊天详情模式,输入框才会出现。

Q: 消息输入了但发送不成功? A: 先用 kind: 'type' 写入 Draft.js 输入框,再点击发送按钮。直接操作 DOM 文本(textContent赋值、execCommandclipboard paste)无法触发 Draft.js 状态。

Q: 怎么确认发送成功了? A: 发送成功后输入框被自动清空(textContent 变为空字符串)。


完整示例

// 1. 打开抖音
browser action=open profile=openclaw targetUrl=https://www.douyin.com/
browser action=act request={"kind": "wait", "timeMs": 3000}

// 2. 点击「私信」按钮
browser action=act request={"kind": "evaluate", "fn": "() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const t = (el.textContent||'').trim(); const r = el.getBoundingClientRect(); if((t === '私信' || t.startsWith('私信')) && r.width > 0 && r.height > 0 && r.width < 100 && r.left > window.innerWidth - 200) { el.click(); return 'clicked: ' + t; } } return 'not found'; }"}
browser action=act request={"kind": "wait", "timeMs": 2000}

// 3. 点击目标用户
browser action=act request={"kind": "evaluate", "fn": "() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const t = (el.textContent||'').trim(); const r = el.getBoundingClientRect(); if(t.includes('<目标用户名>') && r.width > 0 && r.height > 0) { el.click(); return 'clicked: ' + t; } } return 'not found'; }"}
browser action=act request={"kind": "wait", "timeMs": 2000}

// 4. 写入消息并点击发送
browser action=act request={"kind": "type", "selector": "[contenteditable=\"true\"]", "text": "你好,这是测试消息"}
browser action=act request={"kind": "wait", "timeMs": 500}
browser action=act request={"kind": "evaluate", "fn": "() => { const btn = Array.from(document.querySelectorAll('*')).find(el => { const r = el.getBoundingClientRect(); const cls = String(el.className || ''); return r.width >= 20 && r.width <= 50 && r.height >= 20 && r.height <= 50 && r.left > window.innerWidth * 0.7 && r.top > window.innerHeight * 0.55 && (cls.includes('send') || cls.includes('PygT7Ced')); }); if(!btn) return 'send button not found'; btn.click(); return 'clicked send'; }"}
browser action=act request={"kind": "wait", "timeMs": 2000}

// 5. 确认发送
browser action=act request={"kind": "evaluate", "fn": "() => { const input = document.querySelector('[contenteditable=\"true\"]'); return input ? (input.textContent.length === 0 ? 'sent ✓' : 'not sent: ' + input.textContent) : 'input not found'; }"}