Bilibili Messager | B站私信助手
前置要求 | Prerequisites
使用本技能前请先确认以下条件。
| 条件 | 说明 |
|---|---|
| Browser profile | 必须使用 openclaw profile(已通过 browser 工具启动并登录 Bilibili) |
| 登录状态 | Bilibili 账号已在 openclaw profile 中保持登录态 |
| 账号确认 | 发送操作前,技能会要求用户明确确认目标账号和消息内容 |
为什么需要 openclaw profile?
本技能通过在 Bilibili 私信页面执行 DOM JavaScript(querySelector、innerText、click 等)实现消息读取和发送。这些操作需要浏览器环境持有有效的 Bilibili 登录会话。
openclawprofile 由 OpenClaw 启动和管理,独立于 Morois 的日常浏览器 profile- 技能仅在 Bilibili 私信页(
message.bilibili.com)域内执行 DOM 操作,无法访问其他网站 - 任何对外发送操作均需用户明确确认,不会自动发送
模式 | Mode
读取本 SKILL.md 后,先确认当前任务属于哪种模式,再跳转对应章节执行。
| 模式 | 说明 |
|---|---|
| send | 发送新私信(必须两步验证) |
| reply | 回复现有会话中的消息(必须两步验证) |
| read | 读取聊天记录(只读,不写入任何内容) |
通用原则 | General Principles
- Browser profile:使用
openclawprofile(已登录 Bilibili) - 两步验证:发送类操作(send / reply)必须两步验证,不可跳过
- 只读优先:读取聊天记录使用 read 模式,不执行任何写入操作
- 发送前确认:执行任何对外发送前,必须先向用户确认目标账号和消息内容
发送私信 | Mode: Send
步骤 1:打开私信页并确认登录
browser action=open targetUrl=https://message.bilibili.com/#/whisper
检查页面是否跳转到登录页。若跳转到登录页,停下来让用户完成登录。
步骤 2:找到目标会话
在左侧会话列表中找到目标联系人。可以通过文字搜索或滚动列表。
- 使用
browser action=act evaluate配合 DOM 查询找到目标会话 - 找到后点击进入会话
步骤 3:两步验证(强制,必须执行)
第一步(AI 内部执行,不输出给用户)
- 确认已正确进入目标会话(检查 URL 包含
mid参数或页面标题显示对方名字) - 确认消息内容已准备完毕
- 检查消息长度(不超过 500 字)
第二步(输出给用户,等待明确确认)
向用户汇报以下全部内容,确认前不得写入或发送:
| 汇报项 | 内容 |
|---|---|
| 目标账号 | 会话对方的名字 |
| 消息内容 | 将要发送的完整内容 |
| 字数 | 是否在 500 字以内 |
确认标志:用户明确回复「好」「确认」「发」「发吧」「发送」等。
步骤 4:写入消息
使用 innerText 方式写入(经验证有效):
() => {
var editor = document.querySelector('.content-input, [contenteditable="true"]');
if (!editor) return 'editor not found';
editor.focus();
editor.innerText = '消息内容';
editor.dispatchEvent(new Event('input', {bubbles: true}));
return 'typed';
}
步骤 5:点击发送按钮
() => {
var btns = document.querySelectorAll('button, [class*="btn"]');
for (var btn of btns) {
if ((btn.textContent || '').includes('发送')) {
btn.click();
return 'sent';
}
}
return 'send button not found';
}
注意:
- ❌
navigator.clipboard.writeText()+execCommand('paste')— 对B站无效 - ❌ Enter 键发送 — 对B站无效
- ✅ 直接设置
innerText+ 触发input事件 — 成功
步骤 6:验证发送结果
- 发送成功后,消息会出现在聊天窗口中
- 侧边栏会话列表的预览也会更新
回复消息 | Mode: Reply
回复与发送流程基本相同,区别在于:
- 先用 read 模式确认要回复的消息内容
- 在对应会话中打开对话
- 后续步骤同发送流程(步骤 3 两步验证 → 步骤 4 写入 → 步骤 5 发送)
读取聊天记录 | Mode: Read
步骤 1:打开私信页
browser action=open targetUrl=https://message.bilibili.com/#/whisper
步骤 2:找到并打开目标会话
在左侧会话列表中找到目标联系人,点击进入。
步骤 3:提取聊天记录
⚠️ 核心原则:判断发送方归属,100% 依赖 DOM class 名称,不靠语义内容猜测。
发送方归属规则(重要,必读)
B站消息 DOM 结构说明
B站私信页面上,每条消息是一个 HTML 元素,该元素的 class 属性包含判断归属的关键信息。
class 名称的格式一(我发的消息):
class="_Msg_o7f0t_1 _MsgIsMe_o7f0t_9"
_Msg_o7f0t_1:消息容器(固定前缀_Msg_+ 会话IDo7f0t+ 数字1)_MsgIsMe_o7f0t_9:附加标记,表示"这条消息是当前登录用户发的"
class 名称的格式二(对方发的消息):
class="_Msg_o7f0t_1"
- 只有
_Msg_开头的容器 class,没有_MsgIsMe_标记 - 表示这条消息是对方发的
关于会话ID(上面例子里的 o7f0t):
- 会话ID是B站为每个私信会话分配的随机字符串,不同会话ID不同
- 代码中用
[a-z0-9]+匹配(ID只包含小写字母和数字)
判断规则(4步)
-
找到所有顶级消息容器:
- 用 CSS 选择器
[class*="_Msg_"]找到所有包含_Msg_的元素 - 再用正则
_Msg_[a-z0-9]+_\d+过滤,只保留顶级容器- 匹配成功举例:
_Msg_o7f0t_1、_Msg_abc123_42、_Msg_x_99
- 匹配成功举例:
- 用 CSS 选择器
-
判断发送方归属:
- 如果该元素的 class 里包含
_MsgIsMe_,则这是一条我发的消息(Morois / 当前登录用户) - 如果该元素的 class 里不包含
_MsgIsMe_,则这是一条对方发的消息
- 如果该元素的 class 里包含
-
❌ 禁止用左右位置判断:
- B站的消息布局可能产生误导——例如对方发的消息有时也会渲染在右侧
- 永远不要用"左侧/右侧"或"left/right"作为判断依据
- 只用
_MsgIsMe_有没有出现来判断
具体 class 名称示例
| class 名称 | 含义 |
|---|---|
_Msg_o7f0t_1 | 消息容器,会话ID是 o7f0t,序号1 |
_Msg_o7f0t_1 _MsgIsMe_o7f0t_9 | 我发的消息(morois发的),有 _MsgIsMe_ 标记 |
_Msg_abc123_42 | 消息容器,会话ID是 abc123,序号42,对方发的 |
_Msg_x_7 | 消息容器,会话ID是 x,序号7,对方发的 |
_MsgIsMe_x_15 | 我发的消息(morois发的),会话ID是 x,序号15 |
提取脚本(完整示例)
() => {
const containers = Array.from(document.querySelectorAll('[class*="_Msg_"]'))
.filter(el => el.className.match(/_Msg_[a-z0-9]+_\d+ /));
const results = [];
for (const el of containers) {
const cls = el.className + '';
// 关键判断:有 _MsgIsMe_ → 我发的;没有 → 对方发的
const isMe = cls.includes('_MsgIsMe_');
const sender = isMe ? '【我方/Morois】' : '【对方】';
// 提取时间
const timeEl = el.querySelector('[class*="_Msg__Time_"]');
const time = timeEl ? timeEl.textContent.trim() : '';
// 提取消息内容(可能有多个 Content class,取非重复内容)
const contentEls = el.querySelectorAll('[class*="_Msg__Content_"]');
const texts = [];
for (const c of contentEls) {
const t = (c.textContent || '').trim().replace(/\s+/g, ' ');
// 排除时间格式的内容(如"2026年3月28日 18:00")
if (t && !t.match(/^\d{4}年/)) texts.push(t.slice(0, 200));
}
const uniqueTexts = [...new Set(texts)];
const text = uniqueTexts.join(' | ');
if (text) results.push({ sender, time, text });
}
return results;
}
注意事项
- 保留所有消息类型:文本、视频分享、图片、卡片等,不要因为不是纯文本就丢弃
- 结合时间戳重建时间线
- DOM 结构变化时,参考常见问题 → 页面布局变了
常见问题
找不到编辑器
B站私信页面的编辑器可能出现在不同位置。尝试:
() => {
const selectors = [
'.content-input',
'[contenteditable="true"]',
'[class*="editor"]',
'[class*="input"]'
];
for (const sel of selectors) {
const el = document.querySelector(sel);
if (el) return sel + ' found: ' + el.className;
}
return 'no editor found';
}
找不到发送按钮
发送按钮可能没有稳定的 class 或 ref。查找包含"发送"文字的按钮:
() => {
const btns = document.querySelectorAll('button, [class*="btn"], [class*="send"]');
for (const btn of btns) {
if ((btn.textContent || '').includes('发送')) {
return 'found: ' + btn.className;
}
}
return 'send button not found';
}
页面布局变了(DOM 结构诊断)
如果脚本返回空结果或找不到元素,按以下顺序诊断:
- 先快照:用
browser action=snapshot获取当前页面完整 DOM - 找消息容器:在快照中搜索
_Msg_或message - 找编辑器:搜索
contenteditable、input、editor - 找发送按钮:搜索
发送文字或button - 根据诊断结果:用实际找到的 class/name 重写选择器
诊断脚本:
() => {
return {
msgs: Array.from(document.querySelectorAll('[class*="Msg"]')).slice(0,3).map(el=>el.className),
editor: document.querySelector('[contenteditable="true"]') ? 'found' : 'not found',
btns: Array.from(document.querySelectorAll('button')).map(b=>b.textContent.trim()).filter(t=>t)
};
}
限制与注意事项
- 消息长度限制 500 字,超长需分段发送
- 避免高频发送
- 读取记录时优先保证归属和类型正确,再追求历史深度
- 发送前必须用户明确确认目标账号和消息内容