{"id":"37cde22a-d4d0-452e-8a61-4aa5a56771a9","entityType":"agent","slug":"clawhub-moroiser-bilibili-messager","name":"Bilibili Messager | B站私信助手","canonicalUrl":"https://www.xpersona.co/agent/clawhub-moroiser-bilibili-messager","canonicalPath":"/agent/clawhub-moroiser-bilibili-messager","generatedAt":"2026-10-10T05:40:04.283Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-10-09T23:06:14.082Z","emptyReason":null},"description":"Send Bilibili DMs, reply to messages, read chat history via browser automation. | 通过浏览器自动化发送 B 站私信、回复消息、读私信、看聊天记录。 Skill: Bilibili Messager | B站私信助手 Owner: moroiser Summary: Send Bilibili DMs, reply to messages, read chat history via browser automation. | 通过浏览器自动化发送 B 站私信、回复消息、读私信、看聊天记录。 Tags: latest:1.4.8 Version history: v1.4.8 | 2026-05-04T18:13:16.239Z | user 1. Fixed: _meta.json version consistency with registry. 修复：_meta.json 版本号与 registry 一致。 v1.4.7 | 2026-05-04T18:05:43.979Z | user 1. Fixed: _meta.json version consistency","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.9K downloads reported by the source. Last updated 10/9/2026.","installCommand":"clawhub skill install s1708w2t7w31qf7b0gsy7kamnx83gqa6:bilibili-messager","sourceUrl":"https://clawhub.ai/moroiser/bilibili-messager","homepage":"https://clawhub.ai/moroiser/skills/bilibili-messager","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/moroiser/bilibili-messager","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/moroiser/skills/bilibili-messager","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":58,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Send Bilibili DMs, reply to messages, read chat history via browser automation. | 通过浏览器自动化发送 B 站私信、回复消息、读私信、看聊天记录。 Skill: Bilibili Messager | B站私信助手 Owner: moro"},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-09T23:06:14.082Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T23:06:14.082Z","emptyReason":null},"stars":null,"forks":null,"downloads":1911,"packageName":null,"latestVersion":"1.4.8","tractionLabel":"1.9K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T23:06:14.082Z","emptyReason":null},"lastUpdatedAt":"2026-10-09T23:06:14.082Z","lastCrawledAt":"2026-10-09T23:06:14.082Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-10T23:06:14.082Z","lastVerifiedAt":null,"highlights":[{"version":"1.4.8","createdAt":"2026-05-04T18:13:16.239Z","changelog":"1. Fixed: _meta.json version consistency with registry. 修复：_meta.json 版本号与 registry 一致。","fileCount":3,"zipByteSize":6612},{"version":"1.4.7","createdAt":"2026-05-04T18:05:43.979Z","changelog":"1. Fixed: _meta.json version consistency with registry. 修复：_meta.json 版本号与 registry 一致。","fileCount":2,"zipByteSize":5487},{"version":"1.4.6","createdAt":"2026-05-04T16:52:53.584Z","changelog":"1. Simplified: Cleanup instructions — close tab instead of logging out, suitable for personal use. 简化：清理建议改为关闭标签页，适合私人使用场景。","fileCount":2,"zipByteSize":5487},{"version":"1.4.5","createdAt":"2026-05-04T16:18:03.157Z","changelog":"1. Added: Browser-session credential declaration with account verification and cleanup steps. 新增：浏览器会话凭据声明，含账户确认和使用后清理步骤。 2. Added: Data disclosure for read-mode context exposure. 新增：读取模式上下文暴露的数据披露。","fileCount":2,"zipByteSize":5497},{"version":"1.4.4","createdAt":"2026-04-29T16:25:46.764Z","changelog":"1. Add Prerequisites section declaring openclaw browser profile requirement and security scope. 增加前置要求章节，明确声明 openclaw profile 依赖和安全范围说明。","fileCount":2,"zipByteSize":4904},{"version":"1.4.3","createdAt":"2026-04-29T16:08:44.604Z","changelog":"1. Fix H1 title to match frontmatter name: Bilibili Messager | B站私信助手 (一致性修复). 2. No functional changes, SKILL.md content verified with browser DOM testing.","fileCount":2,"zipByteSize":4541},{"version":"1.4.2","createdAt":"2026-04-29T14:32:06.022Z","changelog":"1. Add bilingual display name for ClawHub page title. 补全 ClawHub 页面双语展示名。","fileCount":2,"zipByteSize":4555},{"version":"1.3.6","createdAt":"2026-04-29T14:29:21.135Z","changelog":"1. Add bilingual display name to ClawHub page title. 补全 ClawHub 页面双语展示名。","fileCount":2,"zipByteSize":4554}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s1708w2t7w31qf7b0gsy7kamnx83gqa6:bilibili-messager","setupComplexity":"low","setupSteps":["Setup complexity is classified as HIGH. You must provision dedicated cloud infrastructure or an isolated VM. Do not run this directly on your local workstation.","Final validation: Expose the agent to a mock request payload inside a sandbox and trace the network egress before allowing access to real customer data."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-moroiser-bilibili-messager/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-moroiser-bilibili-messager/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-moroiser-bilibili-messager/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-moroiser-bilibili-messager/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-moroiser-bilibili-messager/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-moroiser-bilibili-messager/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-10T05:40:04.282Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-moroiser-bilibili-messager/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-moroiser-bilibili-messager/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-moroiser-bilibili-messager/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-moroiser-bilibili-messager/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"high","updatedAt":"2026-10-09T23:06:14.082Z","emptyReason":null},"readme":"Skill: Bilibili Messager | B站私信助手\n\nOwner: moroiser\n\nSummary: Send Bilibili DMs, reply to messages, read chat history via browser automation. | 通过浏览器自动化发送 B 站私信、回复消息、读私信、看聊天记录。\n\nTags: latest:1.4.8\n\nVersion history:\n\nv1.4.8 | 2026-05-04T18:13:16.239Z | user\n\n1. Fixed: _meta.json version consistency with registry. 修复：_meta.json 版本号与 registry 一致。\n\nv1.4.7 | 2026-05-04T18:05:43.979Z | user\n\n1. Fixed: _meta.json version consistency with registry. 修复：_meta.json 版本号与 registry 一致。\n\nv1.4.6 | 2026-05-04T16:52:53.584Z | user\n\n1. Simplified: Cleanup instructions — close tab instead of logging out, suitable for personal use. 简化：清理建议改为关闭标签页，适合私人使用场景。\n\nv1.4.5 | 2026-05-04T16:18:03.157Z | user\n\n1. Added: Browser-session credential declaration with account verification and cleanup steps. 新增：浏览器会话凭据声明，含账户确认和使用后清理步骤。 2. Added: Data disclosure for read-mode context exposure. 新增：读取模式上下文暴露的数据披露。\n\nv1.4.4 | 2026-04-29T16:25:46.764Z | user\n\n1. Add Prerequisites section declaring openclaw browser profile requirement and security scope. 增加前置要求章节，明确声明 openclaw profile 依赖和安全范围说明。\n\nv1.4.3 | 2026-04-29T16:08:44.604Z | user\n\n1. Fix H1 title to match frontmatter name: Bilibili Messager | B站私信助手 (一致性修复). 2. No functional changes, SKILL.md content verified with browser DOM testing.\n\nv1.4.2 | 2026-04-29T14:32:06.022Z | user\n\n1. Add bilingual display name for ClawHub page title. 补全 ClawHub 页面双语展示名。\n\nv1.3.6 | 2026-04-29T14:29:21.135Z | user\n\n1. Add bilingual display name to ClawHub page title. 补全 ClawHub 页面双语展示名。\n\nv1.4.1 | 2026-04-12T14:59:33.676Z | user\n\nClean up description: remove redundant trigger phrase line. 清理描述：删除单独的触发词行。\nEnhance description with natural trigger phrases integrated. 扩充描述：将触发词自然融入 description.\n\nv1.4.0 | 2026-04-12T14:44:02.637Z | user\n\n1. Major restructure: add mode selector (send/reply/read), two-step verification for send/reply. 重大重构：增加模式选择（send/reply/read），发送/回复强制两步验证。\n2. Rewrote ownership rules: use _Msg_[a-z0-9]+_\\d+ as container selector, _MsgIsMe_ as sender indicator. 重写归属规则：用_Msg_[a-z0-9]+_\\d+找容器，_MsgIsMe_判断发送方。\n3. Fix class pattern: changed from hardcoded o7f0t to generic [a-z0-9]+ for session ID. 修复class模式：从写死的o7f0t改为通用的[a-z0-9]+匹配会话ID。\n4. Add detailed FAQ for editor/button not found and DOM diagnosis. 增加常见问题：编辑器/按钮找不到、DOM结构诊断。\n5. Remove incorrect de-identification requirements. 删除错误的去标识化要求。\n\nv1.3.5 | 2026-04-06T13:11:37.667Z | user\n\n1. Fix display name to proper format: Bilibili Messager | B站私信助手. 修复显示名称为正确格式。\n\nv1.3.4 | 2026-04-06T03:33:04.733Z | user\n\nFix subprocess.run(shell=True) in record_audio.py for security compliance. 修复 record_audio.py 中 subprocess.run(shell=True) 调用，提升安全性。\n\nv1.3.3 | 2026-03-24T10:25:46.841Z | user\n\nImprove chat history guidance with more reliable sender detection based on message container signals instead of layout guesses. 优化聊天记录读取指导，改用消息容器信号判断发送方，减少对页面布局猜测的依赖。\nExpand history coverage to include forwarded videos, shared cards, and images as first-class chat records. 扩展聊天记录范围，将转发视频、分享卡片和图片纳入正式聊天记录类型。\nRefine the skill into a cleaner bilingual document with clearer execution principles and validation-backed notes. 将技能整理为更清晰的双语文档，并补充更明确的执行原则与基于实测的说明。\n\nv1.3.2 | 2026-03-12T18:24:38.263Z | user\n\nRefined skill wording and publishing readiness. 优化技能表述并整理为更适合发布的版本。\n\nv1.3.1 | 2026-03-02T19:55:20.665Z | user\n\nOptimized triggers for higher sensitivity: added bilibili dm, message, chat, history keywords. 优化激活词提高灵敏度：增加 bilibili dm、message、chat、history 等关键词。\n\nv1.3.0 | 2026-03-02T14:56:18.616Z | user\n\nStandardize name and description format\n\nv1.2.0 | 2026-03-01T06:21:27.076Z | user\n\n新增：获取聊天记录功能（通过头像位置区分发送方，支持滚动加载历史记录）\n\nv1.1.0 | 2026-03-01T02:58:13.547Z | user\n\nRenamed from bilibili-message to bilibili-messager; English description\n\nArchive index:\n\nArchive v1.4.8: 3 files, 6612 bytes\n\nFiles: _meta.json (136b), skill-card.md (2148b), SKILL.md (12191b)\n\nFile v1.4.8:SKILL.md\n\n---\nname: Bilibili Messager | B站私信助手\ndescription: |\n  Send Bilibili DMs, reply to messages, read chat history via browser automation. | 通过浏览器自动化发送 B 站私信、回复消息、读私信、看聊天记录。\nmetadata:\n  \"openclaw\":\n    requires:\n      bins: [\"browser\"]\n      browser_profiles: [\"openclaw\"]  # 必须使用 openclaw profile，且已登录 Bilibili\n    runtime:\n      credentialExpectations:\n        - \"Bilibili 已登录的浏览器会话（openclaw profile）\"\n      filesystemWrites:\n        - \"~/.openclaw/workspace/skills/bilibili-messager/\"\n        - \"~/.openclaw/workspace/projects/bilibili-messager/\"\n    warning:\n      - \"发送操作需要用户两步验证确认\"\n      - \"读取聊天记录会将私信内容暴露到 agent 上下文，仅读取你愿意分享的内容\"\n      - \"使用独立 openclaw profile，不与其他账号混用\"\n---\n\n# Bilibili Messager | B站私信助手\n\n## ⚠️ 安全声明\n\n本技能通过已登录的 Bilibili 浏览器会话读写私信。\n\n**限制：**\n- 只使用独立的 `openclaw` profile（不混用其他账号）\n- 发送前必须两步验证确认目标账号和消息内容\n- 读取模式仅用于你愿意暴露给 agent 的会话内容\n- 不要用于读取高度敏感或非自愿的私人对话\n\n## 前置要求 | Prerequisites\n\n| 条件 | 说明 |\n|------|------|\n| **Browser profile** | 必须使用 `openclaw` profile（已通过 `browser` 工具启动并登录 Bilibili） |\n| **登录状态** | Bilibili 账号已在 `openclaw` profile 中保持登录态 |\n| **账号确认** | 发送操作前，技能会要求用户明确确认目标账号和消息内容 |\n\n### 为什么需要 openclaw profile？\n\n本技能通过在 Bilibili 私信页面执行 DOM JavaScript（`querySelector`、`innerText`、`click` 等）实现消息读取和发送。这些操作需要浏览器环境持有有效的 Bilibili 登录会话。\n\n- `openclaw` profile 由 OpenClaw 启动和管理，独立于 Morois 的日常浏览器 profile\n- 技能仅在 Bilibili 私信页（`message.bilibili.com`）域内执行 DOM 操作，无法访问其他网站\n- 任何对外发送操作均需用户明确确认，不会自动发送\n\n---\n\n## 模式 | Mode\n\n> 读取本 SKILL.md 后，先确认当前任务属于哪种模式，再跳转对应章节执行。\n\n| 模式 | 说明 |\n|------|------|\n| **send** | 发送新私信（必须两步验证） |\n| **reply** | 回复现有会话中的消息（必须两步验证） |\n| **read** | 读取聊天记录（只读，不写入任何内容） |\n\n---\n\n## 通用原则 | General Principles\n\n1. **Browser profile**：使用 `openclaw` profile（已登录 Bilibili）\n2. **两步验证**：发送类操作（send / reply）必须两步验证，不可跳过\n3. **只读优先**：读取聊天记录使用 read 模式，不执行任何写入操作\n4. **发送前确认**：执行任何对外发送前，必须先向用户确认目标账号和消息内容\n---\n\n## 发送私信 | Mode: Send\n\n### 步骤 1：打开私信页并确认登录\n\n```\nbrowser action=open targetUrl=https://message.bilibili.com/#/whisper\n```\n\n检查页面是否跳转到登录页。若跳转到登录页，停下来让用户完成登录。\n\n### 步骤 2：找到目标会话\n\n在左侧会话列表中找到目标联系人。可以通过文字搜索或滚动列表。\n\n- 使用 `browser action=act evaluate` 配合 DOM 查询找到目标会话\n- 找到后点击进入会话\n\n### 步骤 3：两步验证（强制，必须执行）\n\n#### 第一步（AI 内部执行，不输出给用户）\n\n- 确认已正确进入目标会话（检查 URL 包含 `mid` 参数或页面标题显示对方名字）\n- 确认消息内容已准备完毕\n- 检查消息长度（不超过 500 字）\n\n#### 第二步（输出给用户，等待明确确认）\n\n向用户汇报以下全部内容，**确认前不得写入或发送**：\n\n| 汇报项 | 内容 |\n|--------|------|\n| 目标账号 | 会话对方的名字 |\n| 消息内容 | 将要发送的完整内容 |\n| 字数 | 是否在 500 字以内 |\n\n**确认标志**：用户明确回复「好」「确认」「发」「发吧」「发送」等。\n\n### 步骤 4：写入消息\n\n使用 `innerText` 方式写入（经验证有效）：\n\n```javascript\n() => {\n  var editor = document.querySelector('.content-input, [contenteditable=\"true\"]');\n  if (!editor) return 'editor not found';\n  editor.focus();\n  editor.innerText = '消息内容';\n  editor.dispatchEvent(new Event('input', {bubbles: true}));\n  return 'typed';\n}\n```\n\n### 步骤 5：点击发送按钮\n\n```javascript\n() => {\n  var btns = document.querySelectorAll('button, [class*=\"btn\"]');\n  for (var btn of btns) {\n    if ((btn.textContent || '').includes('发送')) {\n      btn.click();\n      return 'sent';\n    }\n  }\n  return 'send button not found';\n}\n```\n\n**注意**：\n- ❌ `navigator.clipboard.writeText()` + `execCommand('paste')` — 对B站无效\n- ❌ Enter 键发送 — 对B站无效\n- ✅ 直接设置 `innerText` + 触发 `input` 事件 — 成功\n\n### 步骤 6：验证发送结果\n\n- 发送成功后，消息会出现在聊天窗口中\n- 侧边栏会话列表的预览也会更新\n\n---\n\n## 回复消息 | Mode: Reply\n\n回复与发送流程基本相同，区别在于：\n\n1. 先用 **read 模式**确认要回复的消息内容\n2. 在对应会话中打开对话\n3. 后续步骤同发送流程（步骤 3 两步验证 → 步骤 4 写入 → 步骤 5 发送）\n\n---\n\n## 读取聊天记录 | Mode: Read\n\n### 步骤 1：打开私信页\n\n```\nbrowser action=open targetUrl=https://message.bilibili.com/#/whisper\n```\n\n### 步骤 2：找到并打开目标会话\n\n在左侧会话列表中找到目标联系人，点击进入。\n\n### 步骤 3：提取聊天记录\n\n**⚠️ 核心原则：判断发送方归属，100% 依赖 DOM class 名称，不靠语义内容猜测。**\n\n#### 发送方归属规则（重要，必读）\n\n##### B站消息 DOM 结构说明\n\nB站私信页面上，每条消息是一个 HTML 元素，**该元素的 class 属性**包含判断归属的关键信息。\n\n**class 名称的格式一（我发的消息）：**\n```\nclass=\"_Msg_o7f0t_1 _MsgIsMe_o7f0t_9\"\n```\n- `_Msg_o7f0t_1`：消息容器（固定前缀 `_Msg_` + 会话ID `o7f0t` + 数字 `1`）\n- `_MsgIsMe_o7f0t_9`：**附加标记，表示\"这条消息是当前登录用户发的\"**\n\n**class 名称的格式二（对方发的消息）：**\n```\nclass=\"_Msg_o7f0t_1\"\n```\n- 只有 `_Msg_` 开头的容器 class，没有 `_MsgIsMe_` 标记\n- 表示这条消息是对方发的\n\n**关于会话ID（上面例子里的 `o7f0t`）：**\n- 会话ID是B站为每个私信会话分配的随机字符串，不同会话ID不同\n- 代码中用 `[a-z0-9]+` 匹配（ID只包含小写字母和数字）\n\n##### 判断规则（4步）\n\n1. **找到所有顶级消息容器**：\n   - 用 CSS 选择器 `[class*=\"_Msg_\"]` 找到所有包含 `_Msg_` 的元素\n   - 再用正则 `_Msg_[a-z0-9]+_\\d+ ` 过滤，只保留顶级容器\n     - 匹配成功举例：`_Msg_o7f0t_1`、`_Msg_abc123_42`、`_Msg_x_99`\n\n2. **判断发送方归属**：\n   - 如果该元素的 class 里**包含 `_MsgIsMe_`**，则这是一条**我发的消息**（Morois / 当前登录用户）\n   - 如果该元素的 class 里**不包含 `_MsgIsMe_`**，则这是一条**对方发的消息**\n\n3. **❌ 禁止用左右位置判断**：\n   - B站的消息布局可能产生误导——例如对方发的消息有时也会渲染在右侧\n   - 永远不要用\"左侧/右侧\"或\"left/right\"作为判断依据\n   - 只用 `_MsgIsMe_` 有没有出现来判断\n\n\n##### 具体 class 名称示例\n\n| class 名称 | 含义 |\n|-----------|------|\n| `_Msg_o7f0t_1` | 消息容器，会话ID是 `o7f0t`，序号1 |\n| `_Msg_o7f0t_1 _MsgIsMe_o7f0t_9` | 我发的消息（morois发的），有 `_MsgIsMe_` 标记 |\n| `_Msg_abc123_42` | 消息容器，会话ID是 `abc123`，序号42，对方发的 |\n| `_Msg_x_7` | 消息容器，会话ID是 `x`，序号7，对方发的 |\n| `_MsgIsMe_x_15` | 我发的消息（morois发的），会话ID是 `x`，序号15 |\n\n##### 提取脚本（完整示例）\n\n```javascript\n() => {\n  const containers = Array.from(document.querySelectorAll('[class*=\"_Msg_\"]'))\n    .filter(el => el.className.match(/_Msg_[a-z0-9]+_\\d+ /));\n\n  const results = [];\n  for (const el of containers) {\n    const cls = el.className + '';\n\n    // 关键判断：有 _MsgIsMe_ → 我发的；没有 → 对方发的\n    const isMe = cls.includes('_MsgIsMe_');\n    const sender = isMe ? '【我方/Morois】' : '【对方】';\n\n    // 提取时间\n    const timeEl = el.querySelector('[class*=\"_Msg__Time_\"]');\n    const time = timeEl ? timeEl.textContent.trim() : '';\n\n    // 提取消息内容（可能有多个 Content class，取非重复内容）\n    const contentEls = el.querySelectorAll('[class*=\"_Msg__Content_\"]');\n    const texts = [];\n    for (const c of contentEls) {\n      const t = (c.textContent || '').trim().replace(/\\s+/g, ' ');\n      // 排除时间格式的内容（如\"2026年3月28日 18:00\"）\n      if (t && !t.match(/^\\d{4}年/)) texts.push(t.slice(0, 200));\n    }\n    const uniqueTexts = [...new Set(texts)];\n    const text = uniqueTexts.join(' | ');\n\n    if (text) results.push({ sender, time, text });\n  }\n  return results;\n}\n```\n\n##### 注意事项\n\n- 保留所有消息类型：文本、视频分享、图片、卡片等，不要因为不是纯文本就丢弃\n- 结合时间戳重建时间线\n- DOM 结构变化时，参考常见问题 → 页面布局变了\n\n---\n\n## 常见问题\n\n### 找不到编辑器\n\nB站私信页面的编辑器可能出现在不同位置。尝试：\n\n```javascript\n() => {\n  const selectors = [\n    '.content-input',\n    '[contenteditable=\"true\"]',\n    '[class*=\"editor\"]',\n    '[class*=\"input\"]'\n  ];\n  for (const sel of selectors) {\n    const el = document.querySelector(sel);\n    if (el) return sel + ' found: ' + el.className;\n  }\n  return 'no editor found';\n}\n```\n\n### 找不到发送按钮\n\n发送按钮可能没有稳定的 class 或 ref。查找包含\"发送\"文字的按钮：\n\n```javascript\n() => {\n  const btns = document.querySelectorAll('button, [class*=\"btn\"], [class*=\"send\"]');\n  for (const btn of btns) {\n    if ((btn.textContent || '').includes('发送')) {\n      return 'found: ' + btn.className;\n    }\n  }\n  return 'send button not found';\n}\n```\n\n### 页面布局变了（DOM 结构诊断）\n\n如果脚本返回空结果或找不到元素，按以下顺序诊断：\n\n1. **先快照**：用 `browser action=snapshot` 获取当前页面完整 DOM\n2. **找消息容器**：在快照中搜索 `_Msg_` 或 `message`\n3. **找编辑器**：搜索 `contenteditable`、`input`、`editor`\n4. **找发送按钮**：搜索 `发送` 文字或 `button`\n5. **根据诊断结果**：用实际找到的 class/name 重写选择器\n\n诊断脚本：\n```javascript\n() => {\n  return {\n    msgs: Array.from(document.querySelectorAll('[class*=\"Msg\"]')).slice(0,3).map(el=>el.className),\n    editor: document.querySelector('[contenteditable=\"true\"]') ? 'found' : 'not found',\n    btns: Array.from(document.querySelectorAll('button')).map(b=>b.textContent.trim()).filter(t=>t)\n  };\n}\n```\n\n---\n\n## 安全与隐私 | Security & Privacy\n\n### 浏览器会话凭据声明\n\n本技能依赖已登录的 Bilibili 浏览器会话作为账户凭据。使用前请确认：\n\n1. **确认登录账户**：发送操作前，先确认当前登录的是正确的 Bilibili 账户\n2. **使用专用 profile**：必须使用独立的 `openclaw` profile，不与日常浏览器混用\n3. **用完后清理**：\n   - 关闭 `openclaw` profile 的浏览器标签页即可\n   - 不再使用时，可清除该 profile 的 Bilibili 登录态\n\n### 数据披露\n\n- 读取聊天记录会将私信内容暴露到 agent 上下文，仅读取你愿意分享的内容\n- 浏览器自动化操作仅在 Bilibili 域（`message.bilibili.com`）内执行，不访问其他网站\n- 不要用于读取高度敏感或非自愿的私人对话\n\n---\n\n## 限制与注意事项\n\n- 消息长度限制 500 字，超长需分段发送\n- 避免高频发送\n- 读取记录时优先保证归属和类型正确，再追求历史深度\n- 发送前必须用户明确确认目标账号和消息内容\n\nFile v1.4.8:_meta.json\n\n{\n  \"ownerId\": \"kn75k53tcdhqwka2fra5x817p9821zpb\",\n  \"slug\": \"bilibili-messager\",\n  \"version\": \"1.4.8\",\n  \"publishedAt\": 1777918396239\n}\n\nFile v1.4.8:skill-card.md\n\n## Description:\n\nSends Bilibili DMs, replies to messages, and reads chat history through a logged-in browser session.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[moroiser](https://clawhub.ai/user/moroiser)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nExternal users who manage Bilibili conversations use this skill to read selected private-message history and draft, send, or reply to DMs after explicit confirmation.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The agent may read selected Bilibili private-message content into its context.\n\nMitigation: Only read conversations the user is willing to share, avoid highly sensitive or non-consensual private messages, and use a dedicated openclaw browser profile.\n\nRisk: The skill can send DMs from the logged-in Bilibili account.\n\nMitigation: Confirm the logged-in account, recipient, message body, and message length before sending or replying; require explicit user confirmation.\n\nRisk: A shared browser profile could expose the wrong account or unrelated browsing context.\n\nMitigation: Use a dedicated openclaw browser profile and clear the Bilibili login state when the skill is no longer needed.\n\n## Reference(s):\n\n- [ClawHub skill page](https://clawhub.ai/moroiser/skills/bilibili-messager)\n- [Publisher profile](https://clawhub.ai/user/moroiser)\n- [Bilibili private messages](https://message.bilibili.com/#/whisper)\n\n## Skill Output:\n\n**Output Type(s):** [Guidance, Shell commands, Code]\n\n**Output Format:** [Markdown guidance with browser tool commands and JavaScript snippets]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Uses a logged-in openclaw browser profile; outgoing messages require explicit recipient and message confirmation.]\n\n## Skill Version(s):\n\n1.4.8 (source: server release evidence and artifact _meta.json)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.4.7: 2 files, 5487 bytes\n\nFiles: _meta.json (136b), SKILL.md (12191b)\n\nFile v1.4.7:SKILL.md\n\n---\nname: Bilibili Messager | B站私信助手\ndescription: |\n  Send Bilibili DMs, reply to messages, read chat history via browser automation. | 通过浏览器自动化发送 B 站私信、回复消息、读私信、看聊天记录。\nmetadata:\n  \"openclaw\":\n    requires:\n      bins: [\"browser\"]\n      browser_profiles: [\"openclaw\"]  # 必须使用 openclaw profile，且已登录 Bilibili\n    runtime:\n      credentialExpectations:\n        - \"Bilibili 已登录的浏览器会话（openclaw profile）\"\n      filesystemWrites:\n        - \"~/.openclaw/workspace/skills/bilibili-messager/\"\n        - \"~/.openclaw/workspace/projects/bilibili-messager/\"\n    warning:\n      - \"发送操作需要用户两步验证确认\"\n      - \"读取聊天记录会将私信内容暴露到 agent 上下文，仅读取你愿意分享的内容\"\n      - \"使用独立 openclaw profile，不与其他账号混用\"\n---\n\n# Bilibili Messager | B站私信助手\n\n## ⚠️ 安全声明\n\n本技能通过已登录的 Bilibili 浏览器会话读写私信。\n\n**限制：**\n- 只使用独立的 `openclaw` profile（不混用其他账号）\n- 发送前必须两步验证确认目标账号和消息内容\n- 读取模式仅用于你愿意暴露给 agent 的会话内容\n- 不要用于读取高度敏感或非自愿的私人对话\n\n## 前置要求 | Prerequisites\n\n| 条件 | 说明 |\n|------|------|\n| **Browser profile** | 必须使用 `openclaw` profile（已通过 `browser` 工具启动并登录 Bilibili） |\n| **登录状态** | Bilibili 账号已在 `openclaw` profile 中保持登录态 |\n| **账号确认** | 发送操作前，技能会要求用户明确确认目标账号和消息内容 |\n\n### 为什么需要 openclaw profile？\n\n本技能通过在 Bilibili 私信页面执行 DOM JavaScript（`querySelector`、`innerText`、`click` 等）实现消息读取和发送。这些操作需要浏览器环境持有有效的 Bilibili 登录会话。\n\n- `openclaw` profile 由 OpenClaw 启动和管理，独立于 Morois 的日常浏览器 profile\n- 技能仅在 Bilibili 私信页（`message.bilibili.com`）域内执行 DOM 操作，无法访问其他网站\n- 任何对外发送操作均需用户明确确认，不会自动发送\n\n---\n\n## 模式 | Mode\n\n> 读取本 SKILL.md 后，先确认当前任务属于哪种模式，再跳转对应章节执行。\n\n| 模式 | 说明 |\n|------|------|\n| **send** | 发送新私信（必须两步验证） |\n| **reply** | 回复现有会话中的消息（必须两步验证） |\n| **read** | 读取聊天记录（只读，不写入任何内容） |\n\n---\n\n## 通用原则 | General Principles\n\n1. **Browser profile**：使用 `openclaw` profile（已登录 Bilibili）\n2. **两步验证**：发送类操作（send / reply）必须两步验证，不可跳过\n3. **只读优先**：读取聊天记录使用 read 模式，不执行任何写入操作\n4. **发送前确认**：执行任何对外发送前，必须先向用户确认目标账号和消息内容\n---\n\n## 发送私信 | Mode: Send\n\n### 步骤 1：打开私信页并确认登录\n\n```\nbrowser action=open targetUrl=https://message.bilibili.com/#/whisper\n```\n\n检查页面是否跳转到登录页。若跳转到登录页，停下来让用户完成登录。\n\n### 步骤 2：找到目标会话\n\n在左侧会话列表中找到目标联系人。可以通过文字搜索或滚动列表。\n\n- 使用 `browser action=act evaluate` 配合 DOM 查询找到目标会话\n- 找到后点击进入会话\n\n### 步骤 3：两步验证（强制，必须执行）\n\n#### 第一步（AI 内部执行，不输出给用户）\n\n- 确认已正确进入目标会话（检查 URL 包含 `mid` 参数或页面标题显示对方名字）\n- 确认消息内容已准备完毕\n- 检查消息长度（不超过 500 字）\n\n#### 第二步（输出给用户，等待明确确认）\n\n向用户汇报以下全部内容，**确认前不得写入或发送**：\n\n| 汇报项 | 内容 |\n|--------|------|\n| 目标账号 | 会话对方的名字 |\n| 消息内容 | 将要发送的完整内容 |\n| 字数 | 是否在 500 字以内 |\n\n**确认标志**：用户明确回复「好」「确认」「发」「发吧」「发送」等。\n\n### 步骤 4：写入消息\n\n使用 `innerText` 方式写入（经验证有效）：\n\n```javascript\n() => {\n  var editor = document.querySelector('.content-input, [contenteditable=\"true\"]');\n  if (!editor) return 'editor not found';\n  editor.focus();\n  editor.innerText = '消息内容';\n  editor.dispatchEvent(new Event('input', {bubbles: true}));\n  return 'typed';\n}\n```\n\n### 步骤 5：点击发送按钮\n\n```javascript\n() => {\n  var btns = document.querySelectorAll('button, [class*=\"btn\"]');\n  for (var btn of btns) {\n    if ((btn.textContent || '').includes('发送')) {\n      btn.click();\n      return 'sent';\n    }\n  }\n  return 'send button not found';\n}\n```\n\n**注意**：\n- ❌ `navigator.clipboard.writeText()` + `execCommand('paste')` — 对B站无效\n- ❌ Enter 键发送 — 对B站无效\n- ✅ 直接设置 `innerText` + 触发 `input` 事件 — 成功\n\n### 步骤 6：验证发送结果\n\n- 发送成功后，消息会出现在聊天窗口中\n- 侧边栏会话列表的预览也会更新\n\n---\n\n## 回复消息 | Mode: Reply\n\n回复与发送流程基本相同，区别在于：\n\n1. 先用 **read 模式**确认要回复的消息内容\n2. 在对应会话中打开对话\n3. 后续步骤同发送流程（步骤 3 两步验证 → 步骤 4 写入 → 步骤 5 发送）\n\n---\n\n## 读取聊天记录 | Mode: Read\n\n### 步骤 1：打开私信页\n\n```\nbrowser action=open targetUrl=https://message.bilibili.com/#/whisper\n```\n\n### 步骤 2：找到并打开目标会话\n\n在左侧会话列表中找到目标联系人，点击进入。\n\n### 步骤 3：提取聊天记录\n\n**⚠️ 核心原则：判断发送方归属，100% 依赖 DOM class 名称，不靠语义内容猜测。**\n\n#### 发送方归属规则（重要，必读）\n\n##### B站消息 DOM 结构说明\n\nB站私信页面上，每条消息是一个 HTML 元素，**该元素的 class 属性**包含判断归属的关键信息。\n\n**class 名称的格式一（我发的消息）：**\n```\nclass=\"_Msg_o7f0t_1 _MsgIsMe_o7f0t_9\"\n```\n- `_Msg_o7f0t_1`：消息容器（固定前缀 `_Msg_` + 会话ID `o7f0t` + 数字 `1`）\n- `_MsgIsMe_o7f0t_9`：**附加标记，表示\"这条消息是当前登录用户发的\"**\n\n**class 名称的格式二（对方发的消息）：**\n```\nclass=\"_Msg_o7f0t_1\"\n```\n- 只有 `_Msg_` 开头的容器 class，没有 `_MsgIsMe_` 标记\n- 表示这条消息是对方发的\n\n**关于会话ID（上面例子里的 `o7f0t`）：**\n- 会话ID是B站为每个私信会话分配的随机字符串，不同会话ID不同\n- 代码中用 `[a-z0-9]+` 匹配（ID只包含小写字母和数字）\n\n##### 判断规则（4步）\n\n1. **找到所有顶级消息容器**：\n   - 用 CSS 选择器 `[class*=\"_Msg_\"]` 找到所有包含 `_Msg_` 的元素\n   - 再用正则 `_Msg_[a-z0-9]+_\\d+ ` 过滤，只保留顶级容器\n     - 匹配成功举例：`_Msg_o7f0t_1`、`_Msg_abc123_42`、`_Msg_x_99`\n\n2. **判断发送方归属**：\n   - 如果该元素的 class 里**包含 `_MsgIsMe_`**，则这是一条**我发的消息**（Morois / 当前登录用户）\n   - 如果该元素的 class 里**不包含 `_MsgIsMe_`**，则这是一条**对方发的消息**\n\n3. **❌ 禁止用左右位置判断**：\n   - B站的消息布局可能产生误导——例如对方发的消息有时也会渲染在右侧\n   - 永远不要用\"左侧/右侧\"或\"left/right\"作为判断依据\n   - 只用 `_MsgIsMe_` 有没有出现来判断\n\n\n##### 具体 class 名称示例\n\n| class 名称 | 含义 |\n|-----------|------|\n| `_Msg_o7f0t_1` | 消息容器，会话ID是 `o7f0t`，序号1 |\n| `_Msg_o7f0t_1 _MsgIsMe_o7f0t_9` | 我发的消息（morois发的），有 `_MsgIsMe_` 标记 |\n| `_Msg_abc123_42` | 消息容器，会话ID是 `abc123`，序号42，对方发的 |\n| `_Msg_x_7` | 消息容器，会话ID是 `x`，序号7，对方发的 |\n| `_MsgIsMe_x_15` | 我发的消息（morois发的），会话ID是 `x`，序号15 |\n\n##### 提取脚本（完整示例）\n\n```javascript\n() => {\n  const containers = Array.from(document.querySelectorAll('[class*=\"_Msg_\"]'))\n    .filter(el => el.className.match(/_Msg_[a-z0-9]+_\\d+ /));\n\n  const results = [];\n  for (const el of containers) {\n    const cls = el.className + '';\n\n    // 关键判断：有 _MsgIsMe_ → 我发的；没有 → 对方发的\n    const isMe = cls.includes('_MsgIsMe_');\n    const sender = isMe ? '【我方/Morois】' : '【对方】';\n\n    // 提取时间\n    const timeEl = el.querySelector('[class*=\"_Msg__Time_\"]');\n    const time = timeEl ? timeEl.textContent.trim() : '';\n\n    // 提取消息内容（可能有多个 Content class，取非重复内容）\n    const contentEls = el.querySelectorAll('[class*=\"_Msg__Content_\"]');\n    const texts = [];\n    for (const c of contentEls) {\n      const t = (c.textContent || '').trim().replace(/\\s+/g, ' ');\n      // 排除时间格式的内容（如\"2026年3月28日 18:00\"）\n      if (t && !t.match(/^\\d{4}年/)) texts.push(t.slice(0, 200));\n    }\n    const uniqueTexts = [...new Set(texts)];\n    const text = uniqueTexts.join(' | ');\n\n    if (text) results.push({ sender, time, text });\n  }\n  return results;\n}\n```\n\n##### 注意事项\n\n- 保留所有消息类型：文本、视频分享、图片、卡片等，不要因为不是纯文本就丢弃\n- 结合时间戳重建时间线\n- DOM 结构变化时，参考常见问题 → 页面布局变了\n\n---\n\n## 常见问题\n\n### 找不到编辑器\n\nB站私信页面的编辑器可能出现在不同位置。尝试：\n\n```javascript\n() => {\n  const selectors = [\n    '.content-input',\n    '[contenteditable=\"true\"]',\n    '[class*=\"editor\"]',\n    '[class*=\"input\"]'\n  ];\n  for (const sel of selectors) {\n    const el = document.querySelector(sel);\n    if (el) return sel + ' found: ' + el.className;\n  }\n  return 'no editor found';\n}\n```\n\n### 找不到发送按钮\n\n发送按钮可能没有稳定的 class 或 ref。查找包含\"发送\"文字的按钮：\n\n```javascript\n() => {\n  const btns = document.querySelectorAll('button, [class*=\"btn\"], [class*=\"send\"]');\n  for (const btn of btns) {\n    if ((btn.textContent || '').includes('发送')) {\n      return 'found: ' + btn.className;\n    }\n  }\n  return 'send button not found';\n}\n```\n\n### 页面布局变了（DOM 结构诊断）\n\n如果脚本返回空结果或找不到元素，按以下顺序诊断：\n\n1. **先快照**：用 `browser action=snapshot` 获取当前页面完整 DOM\n2. **找消息容器**：在快照中搜索 `_Msg_` 或 `message`\n3. **找编辑器**：搜索 `contenteditable`、`input`、`editor`\n4. **找发送按钮**：搜索 `发送` 文字或 `button`\n5. **根据诊断结果**：用实际找到的 class/name 重写选择器\n\n诊断脚本：\n```javascript\n() => {\n  return {\n    msgs: Array.from(document.querySelectorAll('[class*=\"Msg\"]')).slice(0,3).map(el=>el.className),\n    editor: document.querySelector('[contenteditable=\"true\"]') ? 'found' : 'not found',\n    btns: Array.from(document.querySelectorAll('button')).map(b=>b.textContent.trim()).filter(t=>t)\n  };\n}\n```\n\n---\n\n## 安全与隐私 | Security & Privacy\n\n### 浏览器会话凭据声明\n\n本技能依赖已登录的 Bilibili 浏览器会话作为账户凭据。使用前请确认：\n\n1. **确认登录账户**：发送操作前，先确认当前登录的是正确的 Bilibili 账户\n2. **使用专用 profile**：必须使用独立的 `openclaw` profile，不与日常浏览器混用\n3. **用完后清理**：\n   - 关闭 `openclaw` profile 的浏览器标签页即可\n   - 不再使用时，可清除该 profile 的 Bilibili 登录态\n\n### 数据披露\n\n- 读取聊天记录会将私信内容暴露到 agent 上下文，仅读取你愿意分享的内容\n- 浏览器自动化操作仅在 Bilibili 域（`message.bilibili.com`）内执行，不访问其他网站\n- 不要用于读取高度敏感或非自愿的私人对话\n\n---\n\n## 限制与注意事项\n\n- 消息长度限制 500 字，超长需分段发送\n- 避免高频发送\n- 读取记录时优先保证归属和类型正确，再追求历史深度\n- 发送前必须用户明确确认目标账号和消息内容\n\nFile v1.4.7:_meta.json\n\n{\n  \"ownerId\": \"kn75k53tcdhqwka2fra5x817p9821zpb\",\n  \"slug\": \"bilibili-messager\",\n  \"version\": \"1.4.7\",\n  \"publishedAt\": 1777917943979\n}\n\nArchive v1.4.6: 2 files, 5487 bytes\n\nFiles: _meta.json (136b), SKILL.md (12191b)\n\nFile v1.4.6:SKILL.md\n\n---\nname: Bilibili Messager | B站私信助手\ndescription: |\n  Send Bilibili DMs, reply to messages, read chat history via browser automation. | 通过浏览器自动化发送 B 站私信、回复消息、读私信、看聊天记录。\nmetadata:\n  \"openclaw\":\n    requires:\n      bins: [\"browser\"]\n      browser_profiles: [\"openclaw\"]  # 必须使用 openclaw profile，且已登录 Bilibili\n    runtime:\n      credentialExpectations:\n        - \"Bilibili 已登录的浏览器会话（openclaw profile）\"\n      filesystemWrites:\n        - \"~/.openclaw/workspace/skills/bilibili-messager/\"\n        - \"~/.openclaw/workspace/projects/bilibili-messager/\"\n    warning:\n      - \"发送操作需要用户两步验证确认\"\n      - \"读取聊天记录会将私信内容暴露到 agent 上下文，仅读取你愿意分享的内容\"\n      - \"使用独立 openclaw profile，不与其他账号混用\"\n---\n\n# Bilibili Messager | B站私信助手\n\n## ⚠️ 安全声明\n\n本技能通过已登录的 Bilibili 浏览器会话读写私信。\n\n**限制：**\n- 只使用独立的 `openclaw` profile（不混用其他账号）\n- 发送前必须两步验证确认目标账号和消息内容\n- 读取模式仅用于你愿意暴露给 agent 的会话内容\n- 不要用于读取高度敏感或非自愿的私人对话\n\n## 前置要求 | Prerequisites\n\n| 条件 | 说明 |\n|------|------|\n| **Browser profile** | 必须使用 `openclaw` profile（已通过 `browser` 工具启动并登录 Bilibili） |\n| **登录状态** | Bilibili 账号已在 `openclaw` profile 中保持登录态 |\n| **账号确认** | 发送操作前，技能会要求用户明确确认目标账号和消息内容 |\n\n### 为什么需要 openclaw profile？\n\n本技能通过在 Bilibili 私信页面执行 DOM JavaScript（`querySelector`、`innerText`、`click` 等）实现消息读取和发送。这些操作需要浏览器环境持有有效的 Bilibili 登录会话。\n\n- `openclaw` profile 由 OpenClaw 启动和管理，独立于 Morois 的日常浏览器 profile\n- 技能仅在 Bilibili 私信页（`message.bilibili.com`）域内执行 DOM 操作，无法访问其他网站\n- 任何对外发送操作均需用户明确确认，不会自动发送\n\n---\n\n## 模式 | Mode\n\n> 读取本 SKILL.md 后，先确认当前任务属于哪种模式，再跳转对应章节执行。\n\n| 模式 | 说明 |\n|------|------|\n| **send** | 发送新私信（必须两步验证） |\n| **reply** | 回复现有会话中的消息（必须两步验证） |\n| **read** | 读取聊天记录（只读，不写入任何内容） |\n\n---\n\n## 通用原则 | General Principles\n\n1. **Browser profile**：使用 `openclaw` profile（已登录 Bilibili）\n2. **两步验证**：发送类操作（send / reply）必须两步验证，不可跳过\n3. **只读优先**：读取聊天记录使用 read 模式，不执行任何写入操作\n4. **发送前确认**：执行任何对外发送前，必须先向用户确认目标账号和消息内容\n---\n\n## 发送私信 | Mode: Send\n\n### 步骤 1：打开私信页并确认登录\n\n```\nbrowser action=open targetUrl=https://message.bilibili.com/#/whisper\n```\n\n检查页面是否跳转到登录页。若跳转到登录页，停下来让用户完成登录。\n\n### 步骤 2：找到目标会话\n\n在左侧会话列表中找到目标联系人。可以通过文字搜索或滚动列表。\n\n- 使用 `browser action=act evaluate` 配合 DOM 查询找到目标会话\n- 找到后点击进入会话\n\n### 步骤 3：两步验证（强制，必须执行）\n\n#### 第一步（AI 内部执行，不输出给用户）\n\n- 确认已正确进入目标会话（检查 URL 包含 `mid` 参数或页面标题显示对方名字）\n- 确认消息内容已准备完毕\n- 检查消息长度（不超过 500 字）\n\n#### 第二步（输出给用户，等待明确确认）\n\n向用户汇报以下全部内容，**确认前不得写入或发送**：\n\n| 汇报项 | 内容 |\n|--------|------|\n| 目标账号 | 会话对方的名字 |\n| 消息内容 | 将要发送的完整内容 |\n| 字数 | 是否在 500 字以内 |\n\n**确认标志**：用户明确回复「好」「确认」「发」「发吧」「发送」等。\n\n### 步骤 4：写入消息\n\n使用 `innerText` 方式写入（经验证有效）：\n\n```javascript\n() => {\n  var editor = document.querySelector('.content-input, [contenteditable=\"true\"]');\n  if (!editor) return 'editor not found';\n  editor.focus();\n  editor.innerText = '消息内容';\n  editor.dispatchEvent(new Event('input', {bubbles: true}));\n  return 'typed';\n}\n```\n\n### 步骤 5：点击发送按钮\n\n```javascript\n() => {\n  var btns = document.querySelectorAll('button, [class*=\"btn\"]');\n  for (var btn of btns) {\n    if ((btn.textContent || '').includes('发送')) {\n      btn.click();\n      return 'sent';\n    }\n  }\n  return 'send button not found';\n}\n```\n\n**注意**：\n- ❌ `navigator.clipboard.writeText()` + `execCommand('paste')` — 对B站无效\n- ❌ Enter 键发送 — 对B站无效\n- ✅ 直接设置 `innerText` + 触发 `input` 事件 — 成功\n\n### 步骤 6：验证发送结果\n\n- 发送成功后，消息会出现在聊天窗口中\n- 侧边栏会话列表的预览也会更新\n\n---\n\n## 回复消息 | Mode: Reply\n\n回复与发送流程基本相同，区别在于：\n\n1. 先用 **read 模式**确认要回复的消息内容\n2. 在对应会话中打开对话\n3. 后续步骤同发送流程（步骤 3 两步验证 → 步骤 4 写入 → 步骤 5 发送）\n\n---\n\n## 读取聊天记录 | Mode: Read\n\n### 步骤 1：打开私信页\n\n```\nbrowser action=open targetUrl=https://message.bilibili.com/#/whisper\n```\n\n### 步骤 2：找到并打开目标会话\n\n在左侧会话列表中找到目标联系人，点击进入。\n\n### 步骤 3：提取聊天记录\n\n**⚠️ 核心原则：判断发送方归属，100% 依赖 DOM class 名称，不靠语义内容猜测。**\n\n#### 发送方归属规则（重要，必读）\n\n##### B站消息 DOM 结构说明\n\nB站私信页面上，每条消息是一个 HTML 元素，**该元素的 class 属性**包含判断归属的关键信息。\n\n**class 名称的格式一（我发的消息）：**\n```\nclass=\"_Msg_o7f0t_1 _MsgIsMe_o7f0t_9\"\n```\n- `_Msg_o7f0t_1`：消息容器（固定前缀 `_Msg_` + 会话ID `o7f0t` + 数字 `1`）\n- `_MsgIsMe_o7f0t_9`：**附加标记，表示\"这条消息是当前登录用户发的\"**\n\n**class 名称的格式二（对方发的消息）：**\n```\nclass=\"_Msg_o7f0t_1\"\n```\n- 只有 `_Msg_` 开头的容器 class，没有 `_MsgIsMe_` 标记\n- 表示这条消息是对方发的\n\n**关于会话ID（上面例子里的 `o7f0t`）：**\n- 会话ID是B站为每个私信会话分配的随机字符串，不同会话ID不同\n- 代码中用 `[a-z0-9]+` 匹配（ID只包含小写字母和数字）\n\n##### 判断规则（4步）\n\n1. **找到所有顶级消息容器**：\n   - 用 CSS 选择器 `[class*=\"_Msg_\"]` 找到所有包含 `_Msg_` 的元素\n   - 再用正则 `_Msg_[a-z0-9]+_\\d+ ` 过滤，只保留顶级容器\n     - 匹配成功举例：`_Msg_o7f0t_1`、`_Msg_abc123_42`、`_Msg_x_99`\n\n2. **判断发送方归属**：\n   - 如果该元素的 class 里**包含 `_MsgIsMe_`**，则这是一条**我发的消息**（Morois / 当前登录用户）\n   - 如果该元素的 class 里**不包含 `_MsgIsMe_`**，则这是一条**对方发的消息**\n\n3. **❌ 禁止用左右位置判断**：\n   - B站的消息布局可能产生误导——例如对方发的消息有时也会渲染在右侧\n   - 永远不要用\"左侧/右侧\"或\"left/right\"作为判断依据\n   - 只用 `_MsgIsMe_` 有没有出现来判断\n\n\n##### 具体 class 名称示例\n\n| class 名称 | 含义 |\n|-----------|------|\n| `_Msg_o7f0t_1` | 消息容器，会话ID是 `o7f0t`，序号1 |\n| `_Msg_o7f0t_1 _MsgIsMe_o7f0t_9` | 我发的消息（morois发的），有 `_MsgIsMe_` 标记 |\n| `_Msg_abc123_42` | 消息容器，会话ID是 `abc123`，序号42，对方发的 |\n| `_Msg_x_7` | 消息容器，会话ID是 `x`，序号7，对方发的 |\n| `_MsgIsMe_x_15` | 我发的消息（morois发的），会话ID是 `x`，序号15 |\n\n##### 提取脚本（完整示例）\n\n```javascript\n() => {\n  const containers = Array.from(document.querySelectorAll('[class*=\"_Msg_\"]'))\n    .filter(el => el.className.match(/_Msg_[a-z0-9]+_\\d+ /));\n\n  const results = [];\n  for (const el of containers) {\n    const cls = el.className + '';\n\n    // 关键判断：有 _MsgIsMe_ → 我发的；没有 → 对方发的\n    const isMe = cls.includes('_MsgIsMe_');\n    const sender = isMe ? '【我方/Morois】' : '【对方】';\n\n    // 提取时间\n    const timeEl = el.querySelector('[class*=\"_Msg__Time_\"]');\n    const time = timeEl ? timeEl.textContent.trim() : '';\n\n    // 提取消息内容（可能有多个 Content class，取非重复内容）\n    const contentEls = el.querySelectorAll('[class*=\"_Msg__Content_\"]');\n    const texts = [];\n    for (const c of contentEls) {\n      const t = (c.textContent || '').trim().replace(/\\s+/g, ' ');\n      // 排除时间格式的内容（如\"2026年3月28日 18:00\"）\n      if (t && !t.match(/^\\d{4}年/)) texts.push(t.slice(0, 200));\n    }\n    const uniqueTexts = [...new Set(texts)];\n    const text = uniqueTexts.join(' | ');\n\n    if (text) results.push({ sender, time, text });\n  }\n  return results;\n}\n```\n\n##### 注意事项\n\n- 保留所有消息类型：文本、视频分享、图片、卡片等，不要因为不是纯文本就丢弃\n- 结合时间戳重建时间线\n- DOM 结构变化时，参考常见问题 → 页面布局变了\n\n---\n\n## 常见问题\n\n### 找不到编辑器\n\nB站私信页面的编辑器可能出现在不同位置。尝试：\n\n```javascript\n() => {\n  const selectors = [\n    '.content-input',\n    '[contenteditable=\"true\"]',\n    '[class*=\"editor\"]',\n    '[class*=\"input\"]'\n  ];\n  for (const sel of selectors) {\n    const el = document.querySelector(sel);\n    if (el) return sel + ' found: ' + el.className;\n  }\n  return 'no editor found';\n}\n```\n\n### 找不到发送按钮\n\n发送按钮可能没有稳定的 class 或 ref。查找包含\"发送\"文字的按钮：\n\n```javascript\n() => {\n  const btns = document.querySelectorAll('button, [class*=\"btn\"], [class*=\"send\"]');\n  for (const btn of btns) {\n    if ((btn.textContent || '').includes('发送')) {\n      return 'found: ' + btn.className;\n    }\n  }\n  return 'send button not found';\n}\n```\n\n### 页面布局变了（DOM 结构诊断）\n\n如果脚本返回空结果或找不到元素，按以下顺序诊断：\n\n1. **先快照**：用 `browser action=snapshot` 获取当前页面完整 DOM\n2. **找消息容器**：在快照中搜索 `_Msg_` 或 `message`\n3. **找编辑器**：搜索 `contenteditable`、`input`、`editor`\n4. **找发送按钮**：搜索 `发送` 文字或 `button`\n5. **根据诊断结果**：用实际找到的 class/name 重写选择器\n\n诊断脚本：\n```javascript\n() => {\n  return {\n    msgs: Array.from(document.querySelectorAll('[class*=\"Msg\"]')).slice(0,3).map(el=>el.className),\n    editor: document.querySelector('[contenteditable=\"true\"]') ? 'found' : 'not found',\n    btns: Array.from(document.querySelectorAll('button')).map(b=>b.textContent.trim()).filter(t=>t)\n  };\n}\n```\n\n---\n\n## 安全与隐私 | Security & Privacy\n\n### 浏览器会话凭据声明\n\n本技能依赖已登录的 Bilibili 浏览器会话作为账户凭据。使用前请确认：\n\n1. **确认登录账户**：发送操作前，先确认当前登录的是正确的 Bilibili 账户\n2. **使用专用 profile**：必须使用独立的 `openclaw` profile，不与日常浏览器混用\n3. **用完后清理**：\n   - 关闭 `openclaw` profile 的浏览器标签页即可\n   - 不再使用时，可清除该 profile 的 Bilibili 登录态\n\n### 数据披露\n\n- 读取聊天记录会将私信内容暴露到 agent 上下文，仅读取你愿意分享的内容\n- 浏览器自动化操作仅在 Bilibili 域（`message.bilibili.com`）内执行，不访问其他网站\n- 不要用于读取高度敏感或非自愿的私人对话\n\n---\n\n## 限制与注意事项\n\n- 消息长度限制 500 字，超长需分段发送\n- 避免高频发送\n- 读取记录时优先保证归属和类型正确，再追求历史深度\n- 发送前必须用户明确确认目标账号和消息内容\n\nFile v1.4.6:_meta.json\n\n{\n  \"ownerId\": \"kn75k53tcdhqwka2fra5x817p9821zpb\",\n  \"slug\": \"bilibili-messager\",\n  \"version\": \"1.4.6\",\n  \"publishedAt\": 1777913573584\n}\n\nArchive v1.4.5: 2 files, 5497 bytes\n\nFiles: _meta.json (136b), SKILL.md (12239b)\n\nFile v1.4.5:SKILL.md\n\n---\nname: Bilibili Messager | B站私信助手\ndescription: |\n  Send Bilibili DMs, reply to messages, read chat history via browser automation. | 通过浏览器自动化发送 B 站私信、回复消息、读私信、看聊天记录。\nmetadata:\n  \"openclaw\":\n    requires:\n      bins: [\"browser\"]\n      browser_profiles: [\"openclaw\"]  # 必须使用 openclaw profile，且已登录 Bilibili\n    runtime:\n      credentialExpectations:\n        - \"Bilibili 已登录的浏览器会话（openclaw profile）\"\n      filesystemWrites:\n        - \"~/.openclaw/workspace/skills/bilibili-messager/\"\n        - \"~/.openclaw/workspace/projects/bilibili-messager/\"\n    warning:\n      - \"发送操作需要用户两步验证确认\"\n      - \"读取聊天记录会将私信内容暴露到 agent 上下文，仅读取你愿意分享的内容\"\n      - \"使用独立 openclaw profile，不与其他账号混用\"\n---\n\n# Bilibili Messager | B站私信助手\n\n## ⚠️ 安全声明\n\n本技能通过已登录的 Bilibili 浏览器会话读写私信。\n\n**限制：**\n- 只使用独立的 `openclaw` profile（不混用其他账号）\n- 发送前必须两步验证确认目标账号和消息内容\n- 读取模式仅用于你愿意暴露给 agent 的会话内容\n- 不要用于读取高度敏感或非自愿的私人对话\n\n## 前置要求 | Prerequisites\n\n| 条件 | 说明 |\n|------|------|\n| **Browser profile** | 必须使用 `openclaw` profile（已通过 `browser` 工具启动并登录 Bilibili） |\n| **登录状态** | Bilibili 账号已在 `openclaw` profile 中保持登录态 |\n| **账号确认** | 发送操作前，技能会要求用户明确确认目标账号和消息内容 |\n\n### 为什么需要 openclaw profile？\n\n本技能通过在 Bilibili 私信页面执行 DOM JavaScript（`querySelector`、`innerText`、`click` 等）实现消息读取和发送。这些操作需要浏览器环境持有有效的 Bilibili 登录会话。\n\n- `openclaw` profile 由 OpenClaw 启动和管理，独立于 Morois 的日常浏览器 profile\n- 技能仅在 Bilibili 私信页（`message.bilibili.com`）域内执行 DOM 操作，无法访问其他网站\n- 任何对外发送操作均需用户明确确认，不会自动发送\n\n---\n\n## 模式 | Mode\n\n> 读取本 SKILL.md 后，先确认当前任务属于哪种模式，再跳转对应章节执行。\n\n| 模式 | 说明 |\n|------|------|\n| **send** | 发送新私信（必须两步验证） |\n| **reply** | 回复现有会话中的消息（必须两步验证） |\n| **read** | 读取聊天记录（只读，不写入任何内容） |\n\n---\n\n## 通用原则 | General Principles\n\n1. **Browser profile**：使用 `openclaw` profile（已登录 Bilibili）\n2. **两步验证**：发送类操作（send / reply）必须两步验证，不可跳过\n3. **只读优先**：读取聊天记录使用 read 模式，不执行任何写入操作\n4. **发送前确认**：执行任何对外发送前，必须先向用户确认目标账号和消息内容\n---\n\n## 发送私信 | Mode: Send\n\n### 步骤 1：打开私信页并确认登录\n\n```\nbrowser action=open targetUrl=https://message.bilibili.com/#/whisper\n```\n\n检查页面是否跳转到登录页。若跳转到登录页，停下来让用户完成登录。\n\n### 步骤 2：找到目标会话\n\n在左侧会话列表中找到目标联系人。可以通过文字搜索或滚动列表。\n\n- 使用 `browser action=act evaluate` 配合 DOM 查询找到目标会话\n- 找到后点击进入会话\n\n### 步骤 3：两步验证（强制，必须执行）\n\n#### 第一步（AI 内部执行，不输出给用户）\n\n- 确认已正确进入目标会话（检查 URL 包含 `mid` 参数或页面标题显示对方名字）\n- 确认消息内容已准备完毕\n- 检查消息长度（不超过 500 字）\n\n#### 第二步（输出给用户，等待明确确认）\n\n向用户汇报以下全部内容，**确认前不得写入或发送**：\n\n| 汇报项 | 内容 |\n|--------|------|\n| 目标账号 | 会话对方的名字 |\n| 消息内容 | 将要发送的完整内容 |\n| 字数 | 是否在 500 字以内 |\n\n**确认标志**：用户明确回复「好」「确认」「发」「发吧」「发送」等。\n\n### 步骤 4：写入消息\n\n使用 `innerText` 方式写入（经验证有效）：\n\n```javascript\n() => {\n  var editor = document.querySelector('.content-input, [contenteditable=\"true\"]');\n  if (!editor) return 'editor not found';\n  editor.focus();\n  editor.innerText = '消息内容';\n  editor.dispatchEvent(new Event('input', {bubbles: true}));\n  return 'typed';\n}\n```\n\n### 步骤 5：点击发送按钮\n\n```javascript\n() => {\n  var btns = document.querySelectorAll('button, [class*=\"btn\"]');\n  for (var btn of btns) {\n    if ((btn.textContent || '').includes('发送')) {\n      btn.click();\n      return 'sent';\n    }\n  }\n  return 'send button not found';\n}\n```\n\n**注意**：\n- ❌ `navigator.clipboard.writeText()` + `execCommand('paste')` — 对B站无效\n- ❌ Enter 键发送 — 对B站无效\n- ✅ 直接设置 `innerText` + 触发 `input` 事件 — 成功\n\n### 步骤 6：验证发送结果\n\n- 发送成功后，消息会出现在聊天窗口中\n- 侧边栏会话列表的预览也会更新\n\n---\n\n## 回复消息 | Mode: Reply\n\n回复与发送流程基本相同，区别在于：\n\n1. 先用 **read 模式**确认要回复的消息内容\n2. 在对应会话中打开对话\n3. 后续步骤同发送流程（步骤 3 两步验证 → 步骤 4 写入 → 步骤 5 发送）\n\n---\n\n## 读取聊天记录 | Mode: Read\n\n### 步骤 1：打开私信页\n\n```\nbrowser action=open targetUrl=https://message.bilibili.com/#/whisper\n```\n\n### 步骤 2：找到并打开目标会话\n\n在左侧会话列表中找到目标联系人，点击进入。\n\n### 步骤 3：提取聊天记录\n\n**⚠️ 核心原则：判断发送方归属，100% 依赖 DOM class 名称，不靠语义内容猜测。**\n\n#### 发送方归属规则（重要，必读）\n\n##### B站消息 DOM 结构说明\n\nB站私信页面上，每条消息是一个 HTML 元素，**该元素的 class 属性**包含判断归属的关键信息。\n\n**class 名称的格式一（我发的消息）：**\n```\nclass=\"_Msg_o7f0t_1 _MsgIsMe_o7f0t_9\"\n```\n- `_Msg_o7f0t_1`：消息容器（固定前缀 `_Msg_` + 会话ID `o7f0t` + 数字 `1`）\n- `_MsgIsMe_o7f0t_9`：**附加标记，表示\"这条消息是当前登录用户发的\"**\n\n**class 名称的格式二（对方发的消息）：**\n```\nclass=\"_Msg_o7f0t_1\"\n```\n- 只有 `_Msg_` 开头的容器 class，没有 `_MsgIsMe_` 标记\n- 表示这条消息是对方发的\n\n**关于会话ID（上面例子里的 `o7f0t`）：**\n- 会话ID是B站为每个私信会话分配的随机字符串，不同会话ID不同\n- 代码中用 `[a-z0-9]+` 匹配（ID只包含小写字母和数字）\n\n##### 判断规则（4步）\n\n1. **找到所有顶级消息容器**：\n   - 用 CSS 选择器 `[class*=\"_Msg_\"]` 找到所有包含 `_Msg_` 的元素\n   - 再用正则 `_Msg_[a-z0-9]+_\\d+ ` 过滤，只保留顶级容器\n     - 匹配成功举例：`_Msg_o7f0t_1`、`_Msg_abc123_42`、`_Msg_x_99`\n\n2. **判断发送方归属**：\n   - 如果该元素的 class 里**包含 `_MsgIsMe_`**，则这是一条**我发的消息**（Morois / 当前登录用户）\n   - 如果该元素的 class 里**不包含 `_MsgIsMe_`**，则这是一条**对方发的消息**\n\n3. **❌ 禁止用左右位置判断**：\n   - B站的消息布局可能产生误导——例如对方发的消息有时也会渲染在右侧\n   - 永远不要用\"左侧/右侧\"或\"left/right\"作为判断依据\n   - 只用 `_MsgIsMe_` 有没有出现来判断\n\n\n##### 具体 class 名称示例\n\n| class 名称 | 含义 |\n|-----------|------|\n| `_Msg_o7f0t_1` | 消息容器，会话ID是 `o7f0t`，序号1 |\n| `_Msg_o7f0t_1 _MsgIsMe_o7f0t_9` | 我发的消息（morois发的），有 `_MsgIsMe_` 标记 |\n| `_Msg_abc123_42` | 消息容器，会话ID是 `abc123`，序号42，对方发的 |\n| `_Msg_x_7` | 消息容器，会话ID是 `x`，序号7，对方发的 |\n| `_MsgIsMe_x_15` | 我发的消息（morois发的），会话ID是 `x`，序号15 |\n\n##### 提取脚本（完整示例）\n\n```javascript\n() => {\n  const containers = Array.from(document.querySelectorAll('[class*=\"_Msg_\"]'))\n    .filter(el => el.className.match(/_Msg_[a-z0-9]+_\\d+ /));\n\n  const results = [];\n  for (const el of containers) {\n    const cls = el.className + '';\n\n    // 关键判断：有 _MsgIsMe_ → 我发的；没有 → 对方发的\n    const isMe = cls.includes('_MsgIsMe_');\n    const sender = isMe ? '【我方/Morois】' : '【对方】';\n\n    // 提取时间\n    const timeEl = el.querySelector('[class*=\"_Msg__Time_\"]');\n    const time = timeEl ? timeEl.textContent.trim() : '';\n\n    // 提取消息内容（可能有多个 Content class，取非重复内容）\n    const contentEls = el.querySelectorAll('[class*=\"_Msg__Content_\"]');\n    const texts = [];\n    for (const c of contentEls) {\n      const t = (c.textContent || '').trim().replace(/\\s+/g, ' ');\n      // 排除时间格式的内容（如\"2026年3月28日 18:00\"）\n      if (t && !t.match(/^\\d{4}年/)) texts.push(t.slice(0, 200));\n    }\n    const uniqueTexts = [...new Set(texts)];\n    const text = uniqueTexts.join(' | ');\n\n    if (text) results.push({ sender, time, text });\n  }\n  return results;\n}\n```\n\n##### 注意事项\n\n- 保留所有消息类型：文本、视频分享、图片、卡片等，不要因为不是纯文本就丢弃\n- 结合时间戳重建时间线\n- DOM 结构变化时，参考常见问题 → 页面布局变了\n\n---\n\n## 常见问题\n\n### 找不到编辑器\n\nB站私信页面的编辑器可能出现在不同位置。尝试：\n\n```javascript\n() => {\n  const selectors = [\n    '.content-input',\n    '[contenteditable=\"true\"]',\n    '[class*=\"editor\"]',\n    '[class*=\"input\"]'\n  ];\n  for (const sel of selectors) {\n    const el = document.querySelector(sel);\n    if (el) return sel + ' found: ' + el.className;\n  }\n  return 'no editor found';\n}\n```\n\n### 找不到发送按钮\n\n发送按钮可能没有稳定的 class 或 ref。查找包含\"发送\"文字的按钮：\n\n```javascript\n() => {\n  const btns = document.querySelectorAll('button, [class*=\"btn\"], [class*=\"send\"]');\n  for (const btn of btns) {\n    if ((btn.textContent || '').includes('发送')) {\n      return 'found: ' + btn.className;\n    }\n  }\n  return 'send button not found';\n}\n```\n\n### 页面布局变了（DOM 结构诊断）\n\n如果脚本返回空结果或找不到元素，按以下顺序诊断：\n\n1. **先快照**：用 `browser action=snapshot` 获取当前页面完整 DOM\n2. **找消息容器**：在快照中搜索 `_Msg_` 或 `message`\n3. **找编辑器**：搜索 `contenteditable`、`input`、`editor`\n4. **找发送按钮**：搜索 `发送` 文字或 `button`\n5. **根据诊断结果**：用实际找到的 class/name 重写选择器\n\n诊断脚本：\n```javascript\n() => {\n  return {\n    msgs: Array.from(document.querySelectorAll('[class*=\"Msg\"]')).slice(0,3).map(el=>el.className),\n    editor: document.querySelector('[contenteditable=\"true\"]') ? 'found' : 'not found',\n    btns: Array.from(document.querySelectorAll('button')).map(b=>b.textContent.trim()).filter(t=>t)\n  };\n}\n```\n\n---\n\n## 安全与隐私 | Security & Privacy\n\n### 浏览器会话凭据声明\n\n本技能依赖已登录的 Bilibili 浏览器会话作为账户凭据。使用前请确认：\n\n1. **确认登录账户**：发送操作前，先确认当前登录的是正确的 Bilibili 账户\n2. **使用专用 profile**：必须使用独立的 `openclaw` profile，不与日常浏览器混用\n3. **用完后清理**：\n   - 在 `openclaw` profile 中退出 Bilibili 登录\n   - 或关闭 `openclaw` profile 的浏览器标签页\n   - 不再使用时，清除该 profile 的 Bilibili 登录态\n\n### 数据披露\n\n- 读取聊天记录会将私信内容暴露到 agent 上下文，仅读取你愿意分享的内容\n- 浏览器自动化操作仅在 Bilibili 域（`message.bilibili.com`）内执行，不访问其他网站\n- 不要用于读取高度敏感或非自愿的私人对话\n\n---\n\n## 限制与注意事项\n\n- 消息长度限制 500 字，超长需分段发送\n- 避免高频发送\n- 读取记录时优先保证归属和类型正确，再追求历史深度\n- 发送前必须用户明确确认目标账号和消息内容\n\nFile v1.4.5:_meta.json\n\n{\n  \"ownerId\": \"kn75k53tcdhqwka2fra5x817p9821zpb\",\n  \"slug\": \"bilibili-messager\",\n  \"version\": \"1.4.5\",\n  \"publishedAt\": 1777911483157\n}\n\nArchive v1.4.4: 2 files, 4904 bytes\n\nFiles: _meta.json (136b), SKILL.md (10433b)\n\nFile v1.4.4:SKILL.md\n\n---\nname: Bilibili Messager | B站私信助手\ndescription: |\n  通过浏览器自动化发送 B 站私信、回复消息、读私信、看聊天记录。\n  Send Bilibili DMs, reply to messages, read chat history, and browse conversations via browser automation.\n---\n\n# Bilibili Messager | B站私信助手\n\n## 前置要求 | Prerequisites\n\n> 使用本技能前请先确认以下条件。\n\n| 条件 | 说明 |\n|------|------|\n| **Browser profile** | 必须使用 `openclaw` profile（已通过 `browser` 工具启动并登录 Bilibili） |\n| **登录状态** | Bilibili 账号已在 `openclaw` profile 中保持登录态 |\n| **账号确认** | 发送操作前，技能会要求用户明确确认目标账号和消息内容 |\n\n### 为什么需要 openclaw profile？\n\n本技能通过在 Bilibili 私信页面执行 DOM JavaScript（`querySelector`、`innerText`、`click` 等）实现消息读取和发送。这些操作需要浏览器环境持有有效的 Bilibili 登录会话。\n\n- `openclaw` profile 由 OpenClaw 启动和管理，独立于 Morois 的日常浏览器 profile\n- 技能仅在 Bilibili 私信页（`message.bilibili.com`）域内执行 DOM 操作，无法访问其他网站\n- 任何对外发送操作均需用户明确确认，不会自动发送\n\n---\n\n## 模式 | Mode\n\n> 读取本 SKILL.md 后，先确认当前任务属于哪种模式，再跳转对应章节执行。\n\n| 模式 | 说明 |\n|------|------|\n| **send** | 发送新私信（必须两步验证） |\n| **reply** | 回复现有会话中的消息（必须两步验证） |\n| **read** | 读取聊天记录（只读，不写入任何内容） |\n\n---\n\n## 通用原则 | General Principles\n\n1. **Browser profile**：使用 `openclaw` profile（已登录 Bilibili）\n2. **两步验证**：发送类操作（send / reply）必须两步验证，不可跳过\n3. **只读优先**：读取聊天记录使用 read 模式，不执行任何写入操作\n4. **发送前确认**：执行任何对外发送前，必须先向用户确认目标账号和消息内容\n---\n\n## 发送私信 | Mode: Send\n\n### 步骤 1：打开私信页并确认登录\n\n```\nbrowser action=open targetUrl=https://message.bilibili.com/#/whisper\n```\n\n检查页面是否跳转到登录页。若跳转到登录页，停下来让用户完成登录。\n\n### 步骤 2：找到目标会话\n\n在左侧会话列表中找到目标联系人。可以通过文字搜索或滚动列表。\n\n- 使用 `browser action=act evaluate` 配合 DOM 查询找到目标会话\n- 找到后点击进入会话\n\n### 步骤 3：两步验证（强制，必须执行）\n\n#### 第一步（AI 内部执行，不输出给用户）\n\n- 确认已正确进入目标会话（检查 URL 包含 `mid` 参数或页面标题显示对方名字）\n- 确认消息内容已准备完毕\n- 检查消息长度（不超过 500 字）\n\n#### 第二步（输出给用户，等待明确确认）\n\n向用户汇报以下全部内容，**确认前不得写入或发送**：\n\n| 汇报项 | 内容 |\n|--------|------|\n| 目标账号 | 会话对方的名字 |\n| 消息内容 | 将要发送的完整内容 |\n| 字数 | 是否在 500 字以内 |\n\n**确认标志**：用户明确回复「好」「确认」「发」「发吧」「发送」等。\n\n### 步骤 4：写入消息\n\n使用 `innerText` 方式写入（经验证有效）：\n\n```javascript\n() => {\n  var editor = document.querySelector('.content-input, [contenteditable=\"true\"]');\n  if (!editor) return 'editor not found';\n  editor.focus();\n  editor.innerText = '消息内容';\n  editor.dispatchEvent(new Event('input', {bubbles: true}));\n  return 'typed';\n}\n```\n\n### 步骤 5：点击发送按钮\n\n```javascript\n() => {\n  var btns = document.querySelectorAll('button, [class*=\"btn\"]');\n  for (var btn of btns) {\n    if ((btn.textContent || '').includes('发送')) {\n      btn.click();\n      return 'sent';\n    }\n  }\n  return 'send button not found';\n}\n```\n\n**注意**：\n- ❌ `navigator.clipboard.writeText()` + `execCommand('paste')` — 对B站无效\n- ❌ Enter 键发送 — 对B站无效\n- ✅ 直接设置 `innerText` + 触发 `input` 事件 — 成功\n\n### 步骤 6：验证发送结果\n\n- 发送成功后，消息会出现在聊天窗口中\n- 侧边栏会话列表的预览也会更新\n\n---\n\n## 回复消息 | Mode: Reply\n\n回复与发送流程基本相同，区别在于：\n\n1. 先用 **read 模式**确认要回复的消息内容\n2. 在对应会话中打开对话\n3. 后续步骤同发送流程（步骤 3 两步验证 → 步骤 4 写入 → 步骤 5 发送）\n\n---\n\n## 读取聊天记录 | Mode: Read\n\n### 步骤 1：打开私信页\n\n```\nbrowser action=open targetUrl=https://message.bilibili.com/#/whisper\n```\n\n### 步骤 2：找到并打开目标会话\n\n在左侧会话列表中找到目标联系人，点击进入。\n\n### 步骤 3：提取聊天记录\n\n**⚠️ 核心原则：判断发送方归属，100% 依赖 DOM class 名称，不靠语义内容猜测。**\n\n#### 发送方归属规则（重要，必读）\n\n##### B站消息 DOM 结构说明\n\nB站私信页面上，每条消息是一个 HTML 元素，**该元素的 class 属性**包含判断归属的关键信息。\n\n**class 名称的格式一（我发的消息）：**\n```\nclass=\"_Msg_o7f0t_1 _MsgIsMe_o7f0t_9\"\n```\n- `_Msg_o7f0t_1`：消息容器（固定前缀 `_Msg_` + 会话ID `o7f0t` + 数字 `1`）\n- `_MsgIsMe_o7f0t_9`：**附加标记，表示\"这条消息是当前登录用户发的\"**\n\n**class 名称的格式二（对方发的消息）：**\n```\nclass=\"_Msg_o7f0t_1\"\n```\n- 只有 `_Msg_` 开头的容器 class，没有 `_MsgIsMe_` 标记\n- 表示这条消息是对方发的\n\n**关于会话ID（上面例子里的 `o7f0t`）：**\n- 会话ID是B站为每个私信会话分配的随机字符串，不同会话ID不同\n- 代码中用 `[a-z0-9]+` 匹配（ID只包含小写字母和数字）\n\n##### 判断规则（4步）\n\n1. **找到所有顶级消息容器**：\n   - 用 CSS 选择器 `[class*=\"_Msg_\"]` 找到所有包含 `_Msg_` 的元素\n   - 再用正则 `_Msg_[a-z0-9]+_\\d+ ` 过滤，只保留顶级容器\n     - 匹配成功举例：`_Msg_o7f0t_1`、`_Msg_abc123_42`、`_Msg_x_99`\n\n2. **判断发送方归属**：\n   - 如果该元素的 class 里**包含 `_MsgIsMe_`**，则这是一条**我发的消息**（Morois / 当前登录用户）\n   - 如果该元素的 class 里**不包含 `_MsgIsMe_`**，则这是一条**对方发的消息**\n\n3. **❌ 禁止用左右位置判断**：\n   - B站的消息布局可能产生误导——例如对方发的消息有时也会渲染在右侧\n   - 永远不要用\"左侧/右侧\"或\"left/right\"作为判断依据\n   - 只用 `_MsgIsMe_` 有没有出现来判断\n\n\n##### 具体 class 名称示例\n\n| class 名称 | 含义 |\n|-----------|------|\n| `_Msg_o7f0t_1` | 消息容器，会话ID是 `o7f0t`，序号1 |\n| `_Msg_o7f0t_1 _MsgIsMe_o7f0t_9` | 我发的消息（morois发的），有 `_MsgIsMe_` 标记 |\n| `_Msg_abc123_42` | 消息容器，会话ID是 `abc123`，序号42，对方发的 |\n| `_Msg_x_7` | 消息容器，会话ID是 `x`，序号7，对方发的 |\n| `_MsgIsMe_x_15` | 我发的消息（morois发的），会话ID是 `x`，序号15 |\n\n##### 提取脚本（完整示例）\n\n```javascript\n() => {\n  const containers = Array.from(document.querySelectorAll('[class*=\"_Msg_\"]'))\n    .filter(el => el.className.match(/_Msg_[a-z0-9]+_\\d+ /));\n\n  const results = [];\n  for (const el of containers) {\n    const cls = el.className + '';\n\n    // 关键判断：有 _MsgIsMe_ → 我发的；没有 → 对方发的\n    const isMe = cls.includes('_MsgIsMe_');\n    const sender = isMe ? '【我方/Morois】' : '【对方】';\n\n    // 提取时间\n    const timeEl = el.querySelector('[class*=\"_Msg__Time_\"]');\n    const time = timeEl ? timeEl.textContent.trim() : '';\n\n    // 提取消息内容（可能有多个 Content class，取非重复内容）\n    const contentEls = el.querySelectorAll('[class*=\"_Msg__Content_\"]');\n    const texts = [];\n    for (const c of contentEls) {\n      const t = (c.textContent || '').trim().replace(/\\s+/g, ' ');\n      // 排除时间格式的内容（如\"2026年3月28日 18:00\"）\n      if (t && !t.match(/^\\d{4}年/)) texts.push(t.slice(0, 200));\n    }\n    const uniqueTexts = [...new Set(texts)];\n    const text = uniqueTexts.join(' | ');\n\n    if (text) results.push({ sender, time, text });\n  }\n  return results;\n}\n```\n\n##### 注意事项\n\n- 保留所有消息类型：文本、视频分享、图片、卡片等，不要因为不是纯文本就丢弃\n- 结合时间戳重建时间线\n- DOM 结构变化时，参考常见问题 → 页面布局变了\n\n---\n\n## 常见问题\n\n### 找不到编辑器\n\nB站私信页面的编辑器可能出现在不同位置。尝试：\n\n```javascript\n() => {\n  const selectors = [\n    '.content-input',\n    '[contenteditable=\"true\"]',\n    '[class*=\"editor\"]',\n    '[class*=\"input\"]'\n  ];\n  for (const sel of selectors) {\n    const el = document.querySelector(sel);\n    if (el) return sel + ' found: ' + el.className;\n  }\n  return 'no editor found';\n}\n```\n\n### 找不到发送按钮\n\n发送按钮可能没有稳定的 class 或 ref。查找包含\"发送\"文字的按钮：\n\n```javascript\n() => {\n  const btns = document.querySelectorAll('button, [class*=\"btn\"], [class*=\"send\"]');\n  for (const btn of btns) {\n    if ((btn.textContent || '').includes('发送')) {\n      return 'found: ' + btn.className;\n    }\n  }\n  return 'send button not found';\n}\n```\n\n### 页面布局变了（DOM 结构诊断）\n\n如果脚本返回空结果或找不到元素，按以下顺序诊断：\n\n1. **先快照**：用 `browser action=snapshot` 获取当前页面完整 DOM\n2. **找消息容器**：在快照中搜索 `_Msg_` 或 `message`\n3. **找编辑器**：搜索 `contenteditable`、`input`、`editor`\n4. **找发送按钮**：搜索 `发送` 文字或 `button`\n5. **根据诊断结果**：用实际找到的 class/name 重写选择器\n\n诊断脚本：\n```javascript\n() => {\n  return {\n    msgs: Array.from(document.querySelectorAll('[class*=\"Msg\"]')).slice(0,3).map(el=>el.className),\n    editor: document.querySelector('[contenteditable=\"true\"]') ? 'found' : 'not found',\n    btns: Array.from(document.querySelectorAll('button')).map(b=>b.textContent.trim()).filter(t=>t)\n  };\n}\n```\n\n---\n\n## 限制与注意事项\n\n- 消息长度限制 500 字，超长需分段发送\n- 避免高频发送\n- 读取记录时优先保证归属和类型正确，再追求历史深度\n- 发送前必须用户明确确认目标账号和消息内容\n\nFile v1.4.4:_meta.json\n\n{\n  \"ownerId\": \"kn75k53tcdhqwka2fra5x817p9821zpb\",\n  \"slug\": \"bilibili-messager\",\n  \"version\": \"1.4.4\",\n  \"publishedAt\": 1777479946764\n}\n\nArchive v1.4.3: 2 files, 4541 bytes\n\nFiles: _meta.json (136b), SKILL.md (9473b)\n\nFile v1.4.3:SKILL.md\n\n---\nname: Bilibili Messager | B站私信助手\ndescription: |\n  通过浏览器自动化发送 B 站私信、回复消息、读私信、看聊天记录。\n  Send Bilibili DMs, reply to messages, read chat history, and browse conversations via browser automation.\n---\n\n# Bilibili Messager | B站私信助手\n\n## 模式 | Mode\n\n> 读取本 SKILL.md 后，先确认当前任务属于哪种模式，再跳转对应章节执行。\n\n| 模式 | 说明 |\n|------|------|\n| **send** | 发送新私信（必须两步验证） |\n| **reply** | 回复现有会话中的消息（必须两步验证） |\n| **read** | 读取聊天记录（只读，不写入任何内容） |\n\n---\n\n## 通用原则 | General Principles\n\n1. **Browser profile**：使用 `openclaw` profile（已登录 Bilibili）\n2. **两步验证**：发送类操作（send / reply）必须两步验证，不可跳过\n3. **只读优先**：读取聊天记录使用 read 模式，不执行任何写入操作\n4. **发送前确认**：执行任何对外发送前，必须先向用户确认目标账号和消息内容\n---\n\n## 发送私信 | Mode: Send\n\n### 步骤 1：打开私信页并确认登录\n\n```\nbrowser action=open targetUrl=https://message.bilibili.com/#/whisper\n```\n\n检查页面是否跳转到登录页。若跳转到登录页，停下来让用户完成登录。\n\n### 步骤 2：找到目标会话\n\n在左侧会话列表中找到目标联系人。可以通过文字搜索或滚动列表。\n\n- 使用 `browser action=act evaluate` 配合 DOM 查询找到目标会话\n- 找到后点击进入会话\n\n### 步骤 3：两步验证（强制，必须执行）\n\n#### 第一步（AI 内部执行，不输出给用户）\n\n- 确认已正确进入目标会话（检查 URL 包含 `mid` 参数或页面标题显示对方名字）\n- 确认消息内容已准备完毕\n- 检查消息长度（不超过 500 字）\n\n#### 第二步（输出给用户，等待明确确认）\n\n向用户汇报以下全部内容，**确认前不得写入或发送**：\n\n| 汇报项 | 内容 |\n|--------|------|\n| 目标账号 | 会话对方的名字 |\n| 消息内容 | 将要发送的完整内容 |\n| 字数 | 是否在 500 字以内 |\n\n**确认标志**：用户明确回复「好」「确认」「发」「发吧」「发送」等。\n\n### 步骤 4：写入消息\n\n使用 `innerText` 方式写入（经验证有效）：\n\n```javascript\n() => {\n  var editor = document.querySelector('.content-input, [contenteditable=\"true\"]');\n  if (!editor) return 'editor not found';\n  editor.focus();\n  editor.innerText = '消息内容';\n  editor.dispatchEvent(new Event('input', {bubbles: true}));\n  return 'typed';\n}\n```\n\n### 步骤 5：点击发送按钮\n\n```javascript\n() => {\n  var btns = document.querySelectorAll('button, [class*=\"btn\"]');\n  for (var btn of btns) {\n    if ((btn.textContent || '').includes('发送')) {\n      btn.click();\n      return 'sent';\n    }\n  }\n  return 'send button not found';\n}\n```\n\n**注意**：\n- ❌ `navigator.clipboard.writeText()` + `execCommand('paste')` — 对B站无效\n- ❌ Enter 键发送 — 对B站无效\n- ✅ 直接设置 `innerText` + 触发 `input` 事件 — 成功\n\n### 步骤 6：验证发送结果\n\n- 发送成功后，消息会出现在聊天窗口中\n- 侧边栏会话列表的预览也会更新\n\n---\n\n## 回复消息 | Mode: Reply\n\n回复与发送流程基本相同，区别在于：\n\n1. 先用 **read 模式**确认要回复的消息内容\n2. 在对应会话中打开对话\n3. 后续步骤同发送流程（步骤 3 两步验证 → 步骤 4 写入 → 步骤 5 发送）\n\n---\n\n## 读取聊天记录 | Mode: Read\n\n### 步骤 1：打开私信页\n\n```\nbrowser action=open targetUrl=https://message.bilibili.com/#/whisper\n```\n\n### 步骤 2：找到并打开目标会话\n\n在左侧会话列表中找到目标联系人，点击进入。\n\n### 步骤 3：提取聊天记录\n\n**⚠️ 核心原则：判断发送方归属，100% 依赖 DOM class 名称，不靠语义内容猜测。**\n\n#### 发送方归属规则（重要，必读）\n\n##### B站消息 DOM 结构说明\n\nB站私信页面上，每条消息是一个 HTML 元素，**该元素的 class 属性**包含判断归属的关键信息。\n\n**class 名称的格式一（我发的消息）：**\n```\nclass=\"_Msg_o7f0t_1 _MsgIsMe_o7f0t_9\"\n```\n- `_Msg_o7f0t_1`：消息容器（固定前缀 `_Msg_` + 会话ID `o7f0t` + 数字 `1`）\n- `_MsgIsMe_o7f0t_9`：**附加标记，表示\"这条消息是当前登录用户发的\"**\n\n**class 名称的格式二（对方发的消息）：**\n```\nclass=\"_Msg_o7f0t_1\"\n```\n- 只有 `_Msg_` 开头的容器 class，没有 `_MsgIsMe_` 标记\n- 表示这条消息是对方发的\n\n**关于会话ID（上面例子里的 `o7f0t`）：**\n- 会话ID是B站为每个私信会话分配的随机字符串，不同会话ID不同\n- 代码中用 `[a-z0-9]+` 匹配（ID只包含小写字母和数字）\n\n##### 判断规则（4步）\n\n1. **找到所有顶级消息容器**：\n   - 用 CSS 选择器 `[class*=\"_Msg_\"]` 找到所有包含 `_Msg_` 的元素\n   - 再用正则 `_Msg_[a-z0-9]+_\\d+ ` 过滤，只保留顶级容器\n     - 匹配成功举例：`_Msg_o7f0t_1`、`_Msg_abc123_42`、`_Msg_x_99`\n\n2. **判断发送方归属**：\n   - 如果该元素的 class 里**包含 `_MsgIsMe_`**，则这是一条**我发的消息**（Morois / 当前登录用户）\n   - 如果该元素的 class 里**不包含 `_MsgIsMe_`**，则这是一条**对方发的消息**\n\n3. **❌ 禁止用左右位置判断**：\n   - B站的消息布局可能产生误导——例如对方发的消息有时也会渲染在右侧\n   - 永远不要用\"左侧/右侧\"或\"left/right\"作为判断依据\n   - 只用 `_MsgIsMe_` 有没有出现来判断\n\n\n##### 具体 class 名称示例\n\n| class 名称 | 含义 |\n|-----------|------|\n| `_Msg_o7f0t_1` | 消息容器，会话ID是 `o7f0t`，序号1 |\n| `_Msg_o7f0t_1 _MsgIsMe_o7f0t_9` | 我发的消息（morois发的），有 `_MsgIsMe_` 标记 |\n| `_Msg_abc123_42` | 消息容器，会话ID是 `abc123`，序号42，对方发的 |\n| `_Msg_x_7` | 消息容器，会话ID是 `x`，序号7，对方发的 |\n| `_MsgIsMe_x_15` | 我发的消息（morois发的），会话ID是 `x`，序号15 |\n\n##### 提取脚本（完整示例）\n\n```javascript\n() => {\n  const containers = Array.from(document.querySelectorAll('[class*=\"_Msg_\"]'))\n    .filter(el => el.className.match(/_Msg_[a-z0-9]+_\\d+ /));\n\n  const results = [];\n  for (const el of containers) {\n    const cls = el.className + '';\n\n    // 关键判断：有 _MsgIsMe_ → 我发的；没有 → 对方发的\n    const isMe = cls.includes('_MsgIsMe_');\n    const sender = isMe ? '【我方/Morois】' : '【对方】';\n\n    // 提取时间\n    const timeEl = el.querySelector('[class*=\"_Msg__Time_\"]');\n    const time = timeEl ? timeEl.textContent.trim() : '';\n\n    // 提取消息内容（可能有多个 Content class，取非重复内容）\n    const contentEls = el.querySelectorAll('[class*=\"_Msg__Content_\"]');\n    const texts = [];\n    for (const c of contentEls) {\n      const t = (c.textContent || '').trim().replace(/\\s+/g, ' ');\n      // 排除时间格式的内容（如\"2026年3月28日 18:00\"）\n      if (t && !t.match(/^\\d{4}年/)) texts.push(t.slice(0, 200));\n    }\n    const uniqueTexts = [...new Set(texts)];\n    const text = uniqueTexts.join(' | ');\n\n    if (text) results.push({ sender, time, text });\n  }\n  return results;\n}\n```\n\n##### 注意事项\n\n- 保留所有消息类型：文本、视频分享、图片、卡片等，不要因为不是纯文本就丢弃\n- 结合时间戳重建时间线\n- DOM 结构变化时，参考常见问题 → 页面布局变了\n\n---\n\n## 常见问题\n\n### 找不到编辑器\n\nB站私信页面的编辑器可能出现在不同位置。尝试：\n\n```javascript\n() => {\n  const selectors = [\n    '.content-input',\n    '[contenteditable=\"true\"]',\n    '[class*=\"editor\"]',\n    '[class*=\"input\"]'\n  ];\n  for (const sel of selectors) {\n    const el = document.querySelector(sel);\n    if (el) return sel + ' found: ' + el.className;\n  }\n  return 'no editor found';\n}\n```\n\n### 找不到发送按钮\n\n发送按钮可能没有稳定的 class 或 ref。查找包含\"发送\"文字的按钮：\n\n```javascript\n() => {\n  const btns = document.querySelectorAll('button, [class*=\"btn\"], [class*=\"send\"]');\n  for (const btn of btns) {\n    if ((btn.textContent || '').includes('发送')) {\n      return 'found: ' + btn.className;\n    }\n  }\n  return 'send button not found';\n}\n```\n\n### 页面布局变了（DOM 结构诊断）\n\n如果脚本返回空结果或找不到元素，按以下顺序诊断：\n\n1. **先快照**：用 `browser action=snapshot` 获取当前页面完整 DOM\n2. **找消息容器**：在快照中搜索 `_Msg_` 或 `message`\n3. **找编辑器**：搜索 `contenteditable`、`input`、`editor`\n4. **找发送按钮**：搜索 `发送` 文字或 `button`\n5. **根据诊断结果**：用实际找到的 class/name 重写选择器\n\n诊断脚本：\n```javascript\n() => {\n  return {\n    msgs: Array.from(document.querySelectorAll('[class*=\"Msg\"]')).slice(0,3).map(el=>el.className),\n    editor: document.querySelector('[contenteditable=\"true\"]') ? 'found' : 'not found',\n    btns: Array.from(document.querySelectorAll('button')).map(b=>b.textContent.trim()).filter(t=>t)\n  };\n}\n```\n\n---\n\n## 限制与注意事项\n\n- 消息长度限制 500 字，超长需分段发送\n- 避免高频发送\n- 读取记录时优先保证归属和类型正确，再追求历史深度\n- 发送前必须用户明确确认目标账号和消息内容\n\nFile v1.4.3:_meta.json\n\n{\n  \"ownerId\": \"kn75k53tcdhqwka2fra5x817p9821zpb\",\n  \"slug\": \"bilibili-messager\",\n  \"version\": \"1.4.3\",\n  \"publishedAt\": 1777478924604\n}\n\nArchive v1.4.2: 2 files, 4555 bytes\n\nFiles: _meta.json (136b), SKILL.md (9485b)\n\nFile v1.4.2:SKILL.md\n\n---\nname: Bilibili Messager | B站私信助手\ndescription: |\n  通过浏览器自动化发送 B 站私信、回复消息、读私信、看聊天记录。\n  Send Bilibili DMs, reply to messages, read chat history, and browse conversations via browser automation.\n---\n\n# Bilibili Private Messaging | B站私信自动化\n\n## 模式 | Mode\n\n> 读取本 SKILL.md 后，先确认当前任务属于哪种模式，再跳转对应章节执行。\n\n| 模式 | 说明 |\n|------|------|\n| **send** | 发送新私信（必须两步验证） |\n| **reply** | 回复现有会话中的消息（必须两步验证） |\n| **read** | 读取聊天记录（只读，不写入任何内容） |\n\n---\n\n## 通用原则 | General Principles\n\n1. **Browser profile**：使用 `openclaw` profile（已登录 Bilibili）\n2. **两步验证**：发送类操作（send / reply）必须两步验证，不可跳过\n3. **只读优先**：读取聊天记录使用 read 模式，不执行任何写入操作\n4. **发送前确认**：执行任何对外发送前，必须先向用户确认目标账号和消息内容\n---\n\n## 发送私信 | Mode: Send\n\n### 步骤 1：打开私信页并确认登录\n\n```\nbrowser action=open targetUrl=https://message.bilibili.com/#/whisper\n```\n\n检查页面是否跳转到登录页。若跳转到登录页，停下来让用户完成登录。\n\n### 步骤 2：找到目标会话\n\n在左侧会话列表中找到目标联系人。可以通过文字搜索或滚动列表。\n\n- 使用 `browser action=act evaluate` 配合 DOM 查询找到目标会话\n- 找到后点击进入会话\n\n### 步骤 3：两步验证（强制，必须执行）\n\n#### 第一步（AI 内部执行，不输出给用户）\n\n- 确认已正确进入目标会话（检查 URL 包含 `mid` 参数或页面标题显示对方名字）\n- 确认消息内容已准备完毕\n- 检查消息长度（不超过 500 字）\n\n#### 第二步（输出给用户，等待明确确认）\n\n向用户汇报以下全部内容，**确认前不得写入或发送**：\n\n| 汇报项 | 内容 |\n|--------|------|\n| 目标账号 | 会话对方的名字 |\n| 消息内容 | 将要发送的完整内容 |\n| 字数 | 是否在 500 字以内 |\n\n**确认标志**：用户明确回复「好」「确认」「发」「发吧」「发送」等。\n\n### 步骤 4：写入消息\n\n使用 `innerText` 方式写入（经验证有效）：\n\n```javascript\n() => {\n  var editor = document.querySelector('.content-input, [contenteditable=\"true\"]');\n  if (!editor) return 'editor not found';\n  editor.focus();\n  editor.innerText = '消息内容';\n  editor.dispatchEvent(new Event('input', {bubbles: true}));\n  return 'typed';\n}\n```\n\n### 步骤 5：点击发送按钮\n\n```javascript\n() => {\n  var btns = document.querySelectorAll('button, [class*=\"btn\"]');\n  for (var btn of btns) {\n    if ((btn.textContent || '').includes('发送')) {\n      btn.click();\n      return 'sent';\n    }\n  }\n  return 'send button not found';\n}\n```\n\n**注意**：\n- ❌ `navigator.clipboard.writeText()` + `execCommand('paste')` — 对B站无效\n- ❌ Enter 键发送 — 对B站无效\n- ✅ 直接设置 `innerText` + 触发 `input` 事件 — 成功\n\n### 步骤 6：验证发送结果\n\n- 发送成功后，消息会出现在聊天窗口中\n- 侧边栏会话列表的预览也会更新\n\n---\n\n## 回复消息 | Mode: Reply\n\n回复与发送流程基本相同，区别在于：\n\n1. 先用 **read 模式**确认要回复的消息内容\n2. 在对应会话中打开对话\n3. 后续步骤同发送流程（步骤 3 两步验证 → 步骤 4 写入 → 步骤 5 发送）\n\n---\n\n## 读取聊天记录 | Mode: Read\n\n### 步骤 1：打开私信页\n\n```\nbrowser action=open targetUrl=https://message.bilibili.com/#/whisper\n```\n\n### 步骤 2：找到并打开目标会话\n\n在左侧会话列表中找到目标联系人，点击进入。\n\n### 步骤 3：提取聊天记录\n\n**⚠️ 核心原则：判断发送方归属，100% 依赖 DOM class 名称，不靠语义内容猜测。**\n\n#### 发送方归属规则（重要，必读）\n\n##### B站消息 DOM 结构说明\n\nB站私信页面上，每条消息是一个 HTML 元素，**该元素的 class 属性**包含判断归属的关键信息。\n\n**class 名称的格式一（我发的消息）：**\n```\nclass=\"_Msg_o7f0t_1 _MsgIsMe_o7f0t_9\"\n```\n- `_Msg_o7f0t_1`：消息容器（固定前缀 `_Msg_` + 会话ID `o7f0t` + 数字 `1`）\n- `_MsgIsMe_o7f0t_9`：**附加标记，表示\"这条消息是当前登录用户发的\"**\n\n**class 名称的格式二（对方发的消息）：**\n```\nclass=\"_Msg_o7f0t_1\"\n```\n- 只有 `_Msg_` 开头的容器 class，没有 `_MsgIsMe_` 标记\n- 表示这条消息是对方发的\n\n**关于会话ID（上面例子里的 `o7f0t`）：**\n- 会话ID是B站为每个私信会话分配的随机字符串，不同会话ID不同\n- 代码中用 `[a-z0-9]+` 匹配（ID只包含小写字母和数字）\n\n##### 判断规则（4步）\n\n1. **找到所有顶级消息容器**：\n   - 用 CSS 选择器 `[class*=\"_Msg_\"]` 找到所有包含 `_Msg_` 的元素\n   - 再用正则 `_Msg_[a-z0-9]+_\\d+ ` 过滤，只保留顶级容器\n     - 匹配成功举例：`_Msg_o7f0t_1`、`_Msg_abc123_42`、`_Msg_x_99`\n\n2. **判断发送方归属**：\n   - 如果该元素的 class 里**包含 `_MsgIsMe_`**，则这是一条**我发的消息**（Morois / 当前登录用户）\n   - 如果该元素的 class 里**不包含 `_MsgIsMe_`**，则这是一条**对方发的消息**\n\n3. **❌ 禁止用左右位置判断**：\n   - B站的消息布局可能产生误导——例如对方发的消息有时也会渲染在右侧\n   - 永远不要用\"左侧/右侧\"或\"left/right\"作为判断依据\n   - 只用 `_MsgIsMe_` 有没有出现来判断\n\n\n##### 具体 class 名称示例\n\n| class 名称 | 含义 |\n|-----------|------|\n| `_Msg_o7f0t_1` | 消息容器，会话ID是 `o7f0t`，序号1 |\n| `_Msg_o7f0t_1 _MsgIsMe_o7f0t_9` | 我发的消息（morois发的），有 `_MsgIsMe_` 标记 |\n| `_Msg_abc123_42` | 消息容器，会话ID是 `abc123`，序号42，对方发的 |\n| `_Msg_x_7` | 消息容器，会话ID是 `x`，序号7，对方发的 |\n| `_MsgIsMe_x_15` | 我发的消息（morois发的），会话ID是 `x`，序号15 |\n\n##### 提取脚本（完整示例）\n\n```javascript\n() => {\n  const containers = Array.from(document.querySelectorAll('[class*=\"_Msg_\"]'))\n    .filter(el => el.className.match(/_Msg_[a-z0-9]+_\\d+ /));\n\n  const results = [];\n  for (const el of containers) {\n    const cls = el.className + '';\n\n    // 关键判断：有 _MsgIsMe_ → 我发的；没有 → 对方发的\n    const isMe = cls.includes('_MsgIsMe_');\n    const sender = isMe ? '【我方/Morois】' : '【对方】';\n\n    // 提取时间\n    const timeEl = el.querySelector('[class*=\"_Msg__Time_\"]');\n    const time = timeEl ? timeEl.textContent.trim() : '';\n\n    // 提取消息内容（可能有多个 Content class，取非重复内容）\n    const contentEls = el.querySelectorAll('[class*=\"_Msg__Content_\"]');\n    const texts = [];\n    for (const c of contentEls) {\n      const t = (c.textContent || '').trim().replace(/\\s+/g, ' ');\n      // 排除时间格式的内容（如\"2026年3月28日 18:00\"）\n      if (t && !t.match(/^\\d{4}年/)) texts.push(t.slice(0, 200));\n    }\n    const uniqueTexts = [...new Set(texts)];\n    const text = uniqueTexts.join(' | ');\n\n    if (text) results.push({ sender, time, text });\n  }\n  return results;\n}\n```\n\n##### 注意事项\n\n- 保留所有消息类型：文本、视频分享、图片、卡片等，不要因为不是纯文本就丢弃\n- 结合时间戳重建时间线\n- DOM 结构变化时，参考常见问题 → 页面布局变了\n\n---\n\n## 常见问题\n\n### 找不到编辑器\n\nB站私信页面的编辑器可能出现在不同位置。尝试：\n\n```javascript\n() => {\n  const selectors = [\n    '.content-input',\n    '[contenteditable=\"true\"]',\n    '[class*=\"editor\"]',\n    '[class*=\"input\"]'\n  ];\n  for (const sel of selectors) {\n    const el = document.querySelector(sel);\n    if (el) return sel + ' found: ' + el.className;\n  }\n  return 'no editor found';\n}\n```\n\n### 找不到发送按钮\n\n发送按钮可能没有稳定的 class 或 ref。查找包含\"发送\"文字的按钮：\n\n```javascript\n() => {\n  const btns = document.querySelectorAll('button, [class*=\"btn\"], [class*=\"send\"]');\n  for (const btn of btns) {\n    if ((btn.textContent || '').includes('发送')) {\n      return 'found: ' + btn.className;\n    }\n  }\n  return 'send button not found';\n}\n```\n\n### 页面布局变了（DOM 结构诊断）\n\n如果脚本返回空结果或找不到元素，按以下顺序诊断：\n\n1. **先快照**：用 `browser action=snapshot` 获取当前页面完整 DOM\n2. **找消息容器**：在快照中搜索 `_Msg_` 或 `message`\n3. **找编辑器**：搜索 `contenteditable`、`input`、`editor`\n4. **找发送按钮**：搜索 `发送` 文字或 `button`\n5. **根据诊断结果**：用实际找到的 class/name 重写选择器\n\n诊断脚本：\n```javascript\n() => {\n  return {\n    msgs: Array.from(document.querySelectorAll('[class*=\"Msg\"]')).slice(0,3).map(el=>el.className),\n    editor: document.querySelector('[contenteditable=\"true\"]') ? 'found' : 'not found',\n    btns: Array.from(document.querySelectorAll('button')).map(b=>b.textContent.trim()).filter(t=>t)\n  };\n}\n```\n\n---\n\n## 限制与注意事项\n\n- 消息长度限制 500 字，超长需分段发送\n- 避免高频发送\n- 读取记录时优先保证归属和类型正确，再追求历史深度\n- 发送前必须用户明确确认目标账号和消息内容\n\nFile v1.4.2:_meta.json\n\n{\n  \"ownerId\": \"kn75k53tcdhqwka2fra5x817p9821zpb\",\n  \"slug\": \"bilibili-messager\",\n  \"version\": \"1.4.2\",\n  \"publishedAt\": 1777473126022\n}\n\nArchive v1.3.6: 2 files, 4554 bytes\n\nFiles: _meta.json (136b), SKILL.md (9485b)\n\nFile v1.3.6:SKILL.md\n\n---\nname: Bilibili Messager | B站私信助手\ndescription: |\n  通过浏览器自动化发送 B 站私信、回复消息、读私信、看聊天记录。\n  Send Bilibili DMs, reply to messages, read chat history, and browse conversations via browser automation.\n---\n\n# Bilibili Private Messaging | B站私信自动化\n\n## 模式 | Mode\n\n> 读取本 SKILL.md 后，先确认当前任务属于哪种模式，再跳转对应章节执行。\n\n| 模式 | 说明 |\n|------|------|\n| **send** | 发送新私信（必须两步验证） |\n| **reply** | 回复现有会话中的消息（必须两步验证） |\n| **read** | 读取聊天记录（只读，不写入任何内容） |\n\n---\n\n## 通用原则 | General Principles\n\n1. **Browser profile**：使用 `openclaw` profile（已登录 Bilibili）\n2. **两步验证**：发送类操作（send / reply）必须两步验证，不可跳过\n3. **只读优先**：读取聊天记录使用 read 模式，不执行任何写入操作\n4. **发送前确认**：执行任何对外发送前，必须先向用户确认目标账号和消息内容\n---\n\n## 发送私信 | Mode: Send\n\n### 步骤 1：打开私信页并确认登录\n\n```\nbrowser action=open targetUrl=https://message.bilibili.com/#/whisper\n```\n\n检查页面是否跳转到登录页。若跳转到登录页，停下来让用户完成登录。\n\n### 步骤 2：找到目标会话\n\n在左侧会话列表中找到目标联系人。可以通过文字搜索或滚动列表。\n\n- 使用 `browser action=act evaluate` 配合 DOM 查询找到目标会话\n- 找到后点击进入会话\n\n### 步骤 3：两步验证（强制，必须执行）\n\n#### 第一步（AI 内部执行，不输出给用户）\n\n- 确认已正确进入目标会话（检查 URL 包含 `mid` 参数或页面标题显示对方名字）\n- 确认消息内容已准备完毕\n- 检查消息长度（不超过 500 字）\n\n#### 第二步（输出给用户，等待明确确认）\n\n向用户汇报以下全部内容，**确认前不得写入或发送**：\n\n| 汇报项 | 内容 |\n|--------|------|\n| 目标账号 | 会话对方的名字 |\n| 消息内容 | 将要发送的完整内容 |\n| 字数 | 是否在 500 字以内 |\n\n**确认标志**：用户明确回复「好」「确认」「发」「发吧」「发送」等。\n\n### 步骤 4：写入消息\n\n使用 `innerText` 方式写入（经验证有效）：\n\n```javascript\n() => {\n  var editor = document.querySelector('.content-input, [contenteditable=\"true\"]');\n  if (!editor) return 'editor not found';\n  editor.focus();\n  editor.innerText = '消息内容';\n  editor.dispatchEvent(new Event('input', {bubbles: true}));\n  return 'typed';\n}\n```\n\n### 步骤 5：点击发送按钮\n\n```javascript\n() => {\n  var btns = document.querySelectorAll('button, [class*=\"btn\"]');\n  for (var btn of btns) {\n    if ((btn.textContent || '').includes('发送')) {\n      btn.click();\n      return 'sent';\n    }\n  }\n  return 'send button not found';\n}\n```\n\n**注意**：\n- ❌ `navigator.clipboard.writeText()` + `execCommand('paste')` — 对B站无效\n- ❌ Enter 键发送 — 对B站无效\n- ✅ 直接设置 `innerText` + 触发 `input` 事件 — 成功\n\n### 步骤 6：验证发送结果\n\n- 发送成功后，消息会出现在聊天窗口中\n- 侧边栏会话列表的预览也会更新\n\n---\n\n## 回复消息 | Mode: Reply\n\n回复与发送流程基本相同，区别在于：\n\n1. 先用 **read 模式**确认要回复的消息内容\n2. 在对应会话中打开对话\n3. 后续步骤同发送流程（步骤 3 两步验证 → 步骤 4 写入 → 步骤 5 发送）\n\n---\n\n## 读取聊天记录 | Mode: Read\n\n### 步骤 1：打开私信页\n\n```\nbrowser action=open targetUrl=https://message.bilibili.com/#/whisper\n```\n\n### 步骤 2：找到并打开目标会话\n\n在左侧会话列表中找到目标联系人，点击进入。\n\n### 步骤 3：提取聊天记录\n\n**⚠️ 核心原则：判断发送方归属，100% 依赖 DOM class 名称，不靠语义内容猜测。**\n\n#### 发送方归属规则（重要，必读）\n\n##### B站消息 DOM 结构说明\n\nB站私信页面上，每条消息是一个 HTML 元素，**该元素的 class 属性**包含判断归属的关键信息。\n\n**class 名称的格式一（我发的消息）：**\n```\nclass=\"_Msg_o7f0t_1 _MsgIsMe_o7f0t_9\"\n```\n- `_Msg_o7f0t_1`：消息容器（固定前缀 `_Msg_` + 会话ID `o7f0t` + 数字 `1`）\n- `_MsgIsMe_o7f0t_9`：**附加标记，表示\"这条消息是当前登录用户发的\"**\n\n**class 名称的格式二（对方发的消息）：**\n```\nclass=\"_Msg_o7f0t_1\"\n```\n- 只有 `_Msg_` 开头的容器 class，没有 `_MsgIsMe_` 标记\n- 表示这条消息是对方发的\n\n**关于会话ID（上面例子里的 `o7f0t`）：**\n- 会话ID是B站为每个私信会话分配的随机字符串，不同会话ID不同\n- 代码中用 `[a-z0-9]+` 匹配（ID只包含小写字母和数字）\n\n##### 判断规则（4步）\n\n1. **找到所有顶级消息容器**：\n   - 用 CSS 选择器 `[class*=\"_Msg_\"]` 找到所有包含 `_Msg_` 的元素\n   - 再用正则 `_Msg_[a-z0-9]+_\\d+ ` 过滤，只保留顶级容器\n     - 匹配成功举例：`_Msg_o7f0t_1`、`_Msg_abc123_42`、`_Msg_x_99`\n\n2. **判断发送方归属**：\n   - 如果该元素的 class 里**包含 `_MsgIsMe_`**，则这是一条**我发的消息**（Morois / 当前登录用户）\n   - 如果该元素的 class 里**不包含 `_MsgIsMe_`**，则这是一条**对方发的消息**\n\n3. **❌ 禁止用左右位置判断**：\n   - B站的消息布局可能产生误导——例如对方发的消息有时也会渲染在右侧\n   - 永远不要用\"左侧/右侧\"或\"left/right\"作为判断依据\n   - 只用 `_MsgIsMe_` 有没有出现来判断\n\n\n##### 具体 class 名称示例\n\n| class 名称 | 含义 |\n|-----------|------|\n| `_Msg_o7f0t_1` | 消息容器，会话ID是 `o7f0t`，序号1 |\n| `_Msg_o7f0t_1 _MsgIsMe_o7f0t_9` | 我发的消息（morois发的），有 `_MsgIsMe_` 标记 |\n| `_Msg_abc123_42` | 消息容器，会话ID是 `abc123`，序号42，对方发的 |\n| `_Msg_x_7` | 消息容器，会话ID是 `x`，序号7，对方发的 |\n| `_MsgIsMe_x_15` | 我发的消息（morois发的），会话ID是 `x`，序号15 |\n\n##### 提取脚本（完整示例）\n\n```javascript\n() => {\n  const containers = Array.from(document.querySelectorAll('[class*=\"_Msg_\"]'))\n    .filter(el => el.className.match(/_Msg_[a-z0-9]+_\\d+ /));\n\n  const results = [];\n  for (const el of containers) {\n    const cls = el.className + '';\n\n    // 关键判断：有 _MsgIsMe_ → 我发的；没有 → 对方发的\n    const isMe = cls.includes('_MsgIsMe_');\n    const sender = isMe ? '【我方/Morois】' : '【对方】';\n\n    // 提取时间\n    const timeEl = el.querySelector('[class*=\"_Msg__Time_\"]');\n    const time = timeEl ? timeEl.textContent.trim() : '';\n\n    // 提取消息内容（可能有多个 Content class，取非重复内容）\n    const contentEls = el.querySelectorAll('[class*=\"_Msg__Content_\"]');\n    const texts = [];\n    for (const c of contentEls) {\n      const t = (c.textContent || '').trim().replace(/\\s+/g, ' ');\n      // 排除时间格式的内容（如\"2026年3月28日 18:00\"）\n      if (t && !t.match(/^\\d{4}年/)) texts.push(t.slice(0, 200));\n    }\n    const uniqueTexts = [...new Set(texts)];\n    const text = uniqueTexts.join(' | ');\n\n    if (text) results.push({ sender, time, text });\n  }\n  return results;\n}\n```\n\n##### 注意事项\n\n- 保留所有消息类型：文本、视频分享、图片、卡片等，不要因为不是纯文本就丢弃\n- 结合时间戳重建时间线\n- DOM 结构变化时，参考常见问题 → 页面布局变了\n\n---\n\n## 常见问题\n\n### 找不到编辑器\n\nB站私信页面的编辑器可能出现在不同位置。尝试：\n\n```javascript\n() => {\n  const selectors = [\n    '.content-input',\n    '[contenteditable=\"true\"]',\n    '[class*=\"editor\"]',\n    '[class*=\"input\"]'\n  ];\n  for (const sel of selectors) {\n    const el = document.querySelector(sel);\n    if (el) return sel + ' found: ' + el.className;\n  }\n  return 'no editor found';\n}\n```\n\n### 找不到发送按钮\n\n发送按钮可能没有稳定的 class 或 ref。查找包含\"发送\"文字的按钮：\n\n```javascript\n() => {\n  const btns = document.querySelectorAll('button, [class*=\"btn\"], [class*=\"send\"]');\n  for (const btn of btns) {\n    if ((btn.textContent || '').includes('发送')) {\n      return 'found: ' + btn.className;\n    }\n  }\n  return 'send button not found';\n}\n```\n\n### 页面布局变了（DOM 结构诊断）\n\n如果脚本返回空结果或找不到元素，按以下顺序诊断：\n\n1. **先快照**：用 `browser action=snapshot` 获取当前页面完整 DOM\n2. **找消息容器**：在快照中搜索 `_Msg_` 或 `message`\n3. **找编辑器**：搜索 `contenteditable`、`input`、`editor`\n4. **找发送按钮**：搜索 `发送` 文字或 `button`\n5. **根据诊断结果**：用实际找到的 class/name 重写选择器\n\n诊断脚本：\n```javascript\n() => {\n  return {\n    msgs: Array.from(document.querySelectorAll('[class*=\"Msg\"]')).slice(0,3).map(el=>el.className),\n    editor: document.querySelector('[contenteditable=\"true\"]') ? 'found' : 'not found',\n    btns: Array.from(document.querySelectorAll('button')).map(b=>b.textContent.trim()).filter(t=>t)\n  };\n}\n```\n\n---\n\n## 限制与注意事项\n\n- 消息长度限制 500 字，超长需分段发送\n- 避免高频发送\n- 读取记录时优先保证归属和类型正确，再追求历史深度\n- 发送前必须用户明确确认目标账号和消息内容\n\nFile v1.3.6:_meta.json\n\n{\n  \"ownerId\": \"kn75k53tcdhqwka2fra5x817p9821zpb\",\n  \"slug\": \"bilibili-messager\",\n  \"version\": \"1.3.6\",\n  \"publishedAt\": 1777472961135\n}\n\nArchive v1.4.1: 2 files, 4555 bytes\n\nFiles: _meta.json (136b), SKILL.md (9485b)\n\nFile v1.4.1:SKILL.md\n\n---\nname: Bilibili Messager | B站私信助手\ndescription: |\n  通过浏览器自动化发送 B 站私信、回复消息、读私信、看聊天记录。\n  Send Bilibili DMs, reply to messages, read chat history, and browse conversations via browser automation.\n---\n\n# Bilibili Private Messaging | B站私信自动化\n\n## 模式 | Mode\n\n> 读取本 SKILL.md 后，先确认当前任务属于哪种模式，再跳转对应章节执行。\n\n| 模式 | 说明 |\n|------|------|\n| **send** | 发送新私信（必须两步验证） |\n| **reply** | 回复现有会话中的消息（必须两步验证） |\n| **read** | 读取聊天记录（只读，不写入任何内容） |\n\n---\n\n## 通用原则 | General Principles\n\n1. **Browser profile**：使用 `openclaw` profile（已登录 Bilibili）\n2. **两步验证**：发送类操作（send / reply）必须两步验证，不可跳过\n3. **只读优先**：读取聊天记录使用 read 模式，不执行任何写入操作\n4. **发送前确认**：执行任何对外发送前，必须先向用户确认目标账号和消息内容\n---\n\n## 发送私信 | Mode: Send\n\n### 步骤 1：打开私信页并确认登录\n\n```\nbrowser action=open targetUrl=https://message.bilibili.com/#/whisper\n```\n\n检查页面是否跳转到登录页。若跳转到登录页，停下来让用户完成登录。\n\n### 步骤 2：找到目标会话\n\n在左侧会话列表中找到目标联系人。可以通过文字搜索或滚动列表。\n\n- 使用 `browser action=act evaluate` 配合 DOM 查询找到目标会话\n- 找到后点击进入会话\n\n### 步骤 3：两步验证（强制，必须执行）\n\n#### 第一步（AI 内部执行，不输出给用户）\n\n- 确认已正确进入目标会话（检查 URL 包含 `mid` 参数或页面标题显示对方名字）\n- 确认消息内容已准备完毕\n- 检查消息长度（不超过 500 字）\n\n#### 第二步（输出给用户，等待明确确认）\n\n向用户汇报以下全部内容，**确认前不得写入或发送**：\n\n| 汇报项 | 内容 |\n|--------|------|\n| 目标账号 | 会话对方的名字 |\n| 消息内容 | 将要发送的完整内容 |\n| 字数 | 是否在 500 字以内 |\n\n**确认标志**：用户明确回复「好」「确认」「发」「发吧」「发送」等。\n\n### 步骤 4：写入消息\n\n使用 `innerText` 方式写入（经验证有效）：\n\n```javascript\n() => {\n  var editor = document.querySelector('.content-input, [contenteditable=\"true\"]');\n  if (!editor) return 'editor not found';\n  editor.focus();\n  editor.innerText = '消息内容';\n  editor.dispatchEvent(new Event('input', {bubbles: true}));\n  return 'typed';\n}\n```\n\n### 步骤 5：点击发送按钮\n\n```javascript\n() => {\n  var btns = document.querySelectorAll('button, [class*=\"btn\"]');\n  for (var btn of btns) {\n    if ((btn.textContent || '').includes('发送')) {\n      btn.click();\n      return 'sent';\n    }\n  }\n  return 'send button not found';\n}\n```\n\n**注意**：\n- ❌ `navigator.clipboard.writeText()` + `execCommand('paste')` — 对B站无效\n- ❌ Enter 键发送 — 对B站无效\n- ✅ 直接设置 `innerText` + 触发 `input` 事件 — 成功\n\n### 步骤 6：验证发送结果\n\n- 发送成功后，消息会出现在聊天窗口中\n- 侧边栏会话列表的预览也会更新\n\n---\n\n## 回复消息 | Mode: Reply\n\n回复与发送流程基本相同，区别在于：\n\n1. 先用 **read 模式**确认要回复的消息内容\n2. 在对应会话中打开对话\n3. 后续步骤同发送流程（步骤 3 两步验证 → 步骤 4 写入 → 步骤 5 发送）\n\n---\n\n## 读取聊天记录 | Mode: Read\n\n### 步骤 1：打开私信页\n\n```\nbrowser action=open targetUrl=https://message.bilibili.com/#/whisper\n```\n\n### 步骤 2：找到并打开目标会话\n\n在左侧会话列表中找到目标联系人，点击进入。\n\n### 步骤 3：提取聊天记录\n\n**⚠️ 核心原则：判断发送方归属，100% 依赖 DOM class 名称，不靠语义内容猜测。**\n\n#### 发送方归属规则（重要，必读）\n\n##### B站消息 DOM 结构说明\n\nB站私信页面上，每条消息是一个 HTML 元素，**该元素的 class 属性**包含判断归属的关键信息。\n\n**class 名称的格式一（我发的消息）：**\n```\nclass=\"_Msg_o7f0t_1 _MsgIsMe_o7f0t_9\"\n```\n- `_Msg_o7f0t_1`：消息容器（固定前缀 `_Msg_` + 会话ID `o7f0t` + 数字 `1`）\n- `_MsgIsMe_o7f0t_9`：**附加标记，表示\"这条消息是当前登录用户发的\"**\n\n**class 名称的格式二（对方发的消息）：**\n```\nclass=\"_Msg_o7f0t_1\"\n```\n- 只有 `_Msg_` 开头的容器 class，没有 `_MsgIsMe_` 标记\n- 表示这条消息是对方发的\n\n**关于会话ID（上面例子里的 `o7f0t`）：**\n- 会话ID是B站为每个私信会话分配的随机字符串，不同会话ID不同\n- 代码中用 `[a-z0-9]+` 匹配（ID只包含小写字母和数字）\n\n##### 判断规则（4步）\n\n1. **找到所有顶级消息容器**：\n   - 用 CSS 选择器 `[class*=\"_Msg_\"]` 找到所有包含 `_Msg_` 的元素\n   - 再用正则 `_Msg_[a-z0-9]+_\\d+ ` 过滤，只保留顶级容器\n     - 匹配成功举例：`_Msg_o7f0t_1`、`_Msg_abc123_42`、`_Msg_x_99`\n\n2. **判断发送方归属**：\n   - 如果该元素的 class 里**包含 `_MsgIsMe_`**，则这是一条**我发的消息**（Morois / 当前登录用户）\n   - 如果该元素的 class 里**不包含 `_MsgIsMe_`**，则这是一条**对方发的消息**\n\n3. **❌ 禁止用左右位置判断**：\n   - B站的消息布局可能产生误导——例如对方发的消息有时也会渲染在右侧\n   - 永远不要用\"左侧/右侧\"或\"left/right\"作为判断依据\n   - 只用 `_MsgIsMe_` 有没有出现来判断\n\n\n##### 具体 class 名称示例\n\n| class 名称 | 含义 |\n|-----------|------|\n| `_Msg_o7f0t_1` | 消息容器，会话ID是 `o7f0t`，序号1 |\n| `_Msg_o7f0t_1 _MsgIsMe_o7f0t_9` | 我发的消息（morois发的），有 `_MsgIsMe_` 标记 |\n| `_Msg_abc123_42` | 消息容器，会话ID是 `abc123`，序号42，对方发的 |\n| `_Msg_x_7` | 消息容器，会话ID是 `x`，序号7，对方发的 |\n| `_MsgIsMe_x_15` | 我发的消息（morois发的），会话ID是 `x`，序号15 |\n\n##### 提取脚本（完整示例）\n\n```javascript\n() => {\n  const containers = Array.from(document.querySelectorAll('[class*=\"_Msg_\"]'))\n    .filter(el => el.className.match(/_Msg_[a-z0-9]+_\\d+ /));\n\n  const results = [];\n  for (const el of containers) {\n    const cls = el.className + '';\n\n    // 关键判断：有 _MsgIsMe_ → 我发的；没有 → 对方发的\n    const isMe = cls.includes('_MsgIsMe_');\n    const sender = isMe ? '【我方/Morois】' : '【对方】';\n\n    // 提取时间\n    const timeEl = el.querySelector('[class*=\"_Msg__Time_\"]');\n    const time = timeEl ? timeEl.textContent.trim() : '';\n\n    // 提取消息内容（可能有多个 Content class，取非重复内容）\n    const contentEls = el.querySelectorAll('[class*=\"_Msg__Content_\"]');\n    const texts = [];\n    for (const c of contentEls) {\n      const t = (c.textContent || '').trim().replace(/\\s+/g, ' ');\n      // 排除时间格式的内容（如\"2026年3月28日 18:00\"）\n      if (t && !t.match(/^\\d{4}年/)) texts.push(t.slice(0, 200));\n    }\n    const uniqueTexts = [...new Set(texts)];\n    const text = uniqueTexts.join(' | ');\n\n    if (text) results.push({ sender, time, text });\n  }\n  return results;\n}\n```\n\n##### 注意事项\n\n- 保留所有消息类型：文本、视频分享、图片、卡片等，不要因为不是纯文本就丢弃\n- 结合时间戳重建时间线\n- DOM 结构变化时，参考常见问题 → 页面布局变了\n\n---\n\n## 常见问题\n\n### 找不到编辑器\n\nB站私信页面的编辑器可能出现在不同位置。尝试：\n\n```javascript\n() => {\n  const selectors = [\n    '.content-input',\n    '[contenteditable=\"true\"]',\n    '[class*=\"editor\"]',\n    '[class*=\"input\"]'\n  ];\n  for (const sel of selectors) {\n    const el = document.querySelector(sel);\n    if (el) return sel + ' found: ' + el.className;\n  }\n  return 'no editor found';\n}\n```\n\n### 找不到发送按钮\n\n发送按钮可能没有稳定的 class 或 ref。查找包含\"发送\"文字的按钮：\n\n```javascript\n() => {\n  const btns = document.querySelectorAll('button, [class*=\"btn\"], [class*=\"send\"]');\n  for (const btn of btns) {\n    if ((btn.textContent || '').includes('发送')) {\n      return 'found: ' + btn.className;\n    }\n  }\n  return 'send button not found';\n}\n```\n\n### 页面布局变了（DOM 结构诊断）\n\n如果脚本返回空结果或找不到元素，按以下顺序诊断：\n\n1. **先快照**：用 `browser action=snapshot` 获取当前页面完整 DOM\n2. **找消息容器**：在快照中搜索 `_Msg_` 或 `message`\n3. **找编辑器**：搜索 `contenteditable`、`input`、`editor`\n4. **找发送按钮**：搜索 `发送` 文字或 `button`\n5. **根据诊断结果**：用实际找到的 class/name 重写选择器\n\n诊断脚本：\n```javascript\n() => {\n  return {\n    msgs: Array.from(document.querySelectorAll('[class*=\"Msg\"]')).slice(0,3).map(el=>el.className),\n    editor: document.querySelector('[contenteditable=\"true\"]') ? 'found' : 'not found',\n    btns: Array.from(document.querySelectorAll('button')).map(b=>b.textContent.trim()).filter(t=>t)\n  };\n}\n```\n\n---\n\n## 限制与注意事项\n\n- 消息长度限制 500 字，超长需分段发送\n- 避免高频发送\n- 读取记录时优先保证归属和类型正确，再追求历史深度\n- 发送前必须用户明确确认目标账号和消息内容\n\nFile v1.4.1:_meta.json\n\n{\n  \"ownerId\": \"kn75k53tcdhqwka2fra5x817p9821zpb\",\n  \"slug\": \"bilibili-messager\",\n  \"version\": \"1.4.1\",\n  \"publishedAt\": 1776005973676\n}\n\nArchive v1.4.0: 2 files, 4568 bytes\n\nFiles: _meta.json (136b), SKILL.md (9545b)\n\nFile v1.4.0:SKILL.md\n\n---\nname: Bilibili Messager | B站私信助手\ndescription: |\n  通过浏览器自动化发送 B 站私信、回复消息、读取聊天记录。\n  Send Bilibili DMs, reply to messages, and read chat history through browser automation.\n  触发词：B站私信、Bilibili私信、发私信、读私信、看聊天记录。\n---\n\n# Bilibili Private Messaging | B站私信自动化\n\n## 模式 | Mode\n\n> 读取本 SKILL.md 后，先确认当前任务属于哪种模式，再跳转对应章节执行。\n\n| 模式 | 说明 |\n|------|------|\n| **send** | 发送新私信（必须两步验证） |\n| **reply** | 回复现有会话中的消息（必须两步验证） |\n| **read** | 读取聊天记录（只读，不写入任何内容） |\n\n---\n\n## 通用原则 | General Principles\n\n1. **Browser profile**：使用 `openclaw` profile（已登录 Bilibili）\n2. **两步验证**：发送类操作（send / reply）必须两步验证，不可跳过\n3. **只读优先**：读取聊天记录使用 read 模式，不执行任何写入操作\n4. **发送前确认**：执行任何对外发送前，必须先向用户确认目标账号和消息内容\n---\n\n## 发送私信 | Mode: Send\n\n### 步骤 1：打开私信页并确认登录\n\n```\nbrowser action=open targetUrl=https://message.bilibili.com/#/whisper\n```\n\n检查页面是否跳转到登录页。若跳转到登录页，停下来让用户完成登录。\n\n### 步骤 2：找到目标会话\n\n在左侧会话列表中找到目标联系人。可以通过文字搜索或滚动列表。\n\n- 使用 `browser action=act evaluate` 配合 DOM 查询找到目标会话\n- 找到后点击进入会话\n\n### 步骤 3：两步验证（强制，必须执行）\n\n#### 第一步（AI 内部执行，不输出给用户）\n\n- 确认已正确进入目标会话（检查 URL 包含 `mid` 参数或页面标题显示对方名字）\n- 确认消息内容已准备完毕\n- 检查消息长度（不超过 500 字）\n\n#### 第二步（输出给用户，等待明确确认）\n\n向用户汇报以下全部内容，**确认前不得写入或发送**：\n\n| 汇报项 | 内容 |\n|--------|------|\n| 目标账号 | 会话对方的名字 |\n| 消息内容 | 将要发送的完整内容 |\n| 字数 | 是否在 500 字以内 |\n\n**确认标志**：用户明确回复「好」「确认」「发」「发吧」「发送」等。\n\n### 步骤 4：写入消息\n\n使用 `innerText` 方式写入（经验证有效）：\n\n```javascript\n() => {\n  var editor = document.querySelector('.content-input, [contenteditable=\"true\"]');\n  if (!editor) return 'editor not found';\n  editor.focus();\n  editor.innerText = '消息内容';\n  editor.dispatchEvent(new Event('input', {bubbles: true}));\n  return 'typed';\n}\n```\n\n### 步骤 5：点击发送按钮\n\n```javascript\n() => {\n  var btns = document.querySelectorAll('button, [class*=\"btn\"]');\n  for (var btn of btns) {\n    if ((btn.textContent || '').includes('发送')) {\n      btn.click();\n      return 'sent';\n    }\n  }\n  return 'send button not found';\n}\n```\n\n**注意**：\n- ❌ `navigator.clipboard.writeText()` + `execCommand('paste')` — 对B站无效\n- ❌ Enter 键发送 — 对B站无效\n- ✅ 直接设置 `innerText` + 触发 `input` 事件 — 成功\n\n### 步骤 6：验证发送结果\n\n- 发送成功后，消息会出现在聊天窗口中\n- 侧边栏会话列表的预览也会更新\n\n---\n\n## 回复消息 | Mode: Reply\n\n回复与发送流程基本相同，区别在于：\n\n1. 先用 **read 模式**确认要回复的消息内容\n2. 在对应会话中打开对话\n3. 后续步骤同发送流程（步骤 3 两步验证 → 步骤 4 写入 → 步骤 5 发送）\n\n---\n\n## 读取聊天记录 | Mode: Read\n\n### 步骤 1：打开私信页\n\n```\nbrowser action=open targetUrl=https://message.bilibili.com/#/whisper\n```\n\n### 步骤 2：找到并打开目标会话\n\n在左侧会话列表中找到目标联系人，点击进入。\n\n### 步骤 3：提取聊天记录\n\n**⚠️ 核心原则：判断发送方归属，100% 依赖 DOM class 名称，不靠语义内容猜测。**\n\n#### 发送方归属规则（重要，必读）\n\n##### B站消息 DOM 结构说明\n\nB站私信页面上，每条消息是一个 HTML 元素，**该元素的 class 属性**包含判断归属的关键信息。\n\n**class 名称的格式一（我发的消息）：**\n```\nclass=\"_Msg_o7f0t_1 _MsgIsMe_o7f0t_9\"\n```\n- `_Msg_o7f0t_1`：消息容器（固定前缀 `_Msg_` + 会话ID `o7f0t` + 数字 `1`）\n- `_MsgIsMe_o7f0t_9`：**附加标记，表示\"这条消息是当前登录用户发的\"**\n\n**class 名称的格式二（对方发的消息）：**\n```\nclass=\"_Msg_o7f0t_1\"\n```\n- 只有 `_Msg_` 开头的容器 class，没有 `_MsgIsMe_` 标记\n- 表示这条消息是对方发的\n\n**关于会话ID（上面例子里的 `o7f0t`）：**\n- 会话ID是B站为每个私信会话分配的随机字符串，不同会话ID不同\n- 代码中用 `[a-z0-9]+` 匹配（ID只包含小写字母和数字）\n\n##### 判断规则（4步）\n\n1. **找到所有顶级消息容器**：\n   - 用 CSS 选择器 `[class*=\"_Msg_\"]` 找到所有包含 `_Msg_` 的元素\n   - 再用正则 `_Msg_[a-z0-9]+_\\d+ ` 过滤，只保留顶级容器\n     - 匹配成功举例：`_Msg_o7f0t_1`、`_Msg_abc123_42`、`_Msg_x_99`\n\n2. **判断发送方归属**：\n   - 如果该元素的 class 里**包含 `_MsgIsMe_`**，则这是一条**我发的消息**（Morois / 当前登录用户）\n   - 如果该元素的 class 里**不包含 `_MsgIsMe_`**，则这是一条**对方发的消息**\n\n3. **❌ 禁止用左右位置判断**：\n   - B站的消息布局可能产生误导——例如对方发的消息有时也会渲染在右侧\n   - 永远不要用\"左侧/右侧\"或\"left/right\"作为判断依据\n   - 只用 `_MsgIsMe_` 有没有出现来判断\n\n\n##### 具体 class 名称示例\n\n| class 名称 | 含义 |\n|-----------|------|\n| `_Msg_o7f0t_1` | 消息容器，会话ID是 `o7f0t`，序号1 |\n| `_Msg_o7f0t_1 _MsgIsMe_o7f0t_9` | 我发的消息（morois发的），有 `_MsgIsMe_` 标记 |\n| `_Msg_abc123_42` | 消息容器，会话ID是 `abc123`，序号42，对方发的 |\n| `_Msg_x_7` | 消息容器，会话ID是 `x`，序号7，对方发的 |\n| `_MsgIsMe_x_15` | 我发的消息（morois发的），会话ID是 `x`，序号15 |\n\n##### 提取脚本（完整示例）\n\n```javascript\n() => {\n  const containers = Array.from(document.querySelectorAll('[class*=\"_Msg_\"]'))\n    .filter(el => el.className.match(/_Msg_[a-z0-9]+_\\d+ /));\n\n  const results = [];\n  for (const el of containers) {\n    const cls = el.className + '';\n\n    // 关键判断：有 _MsgIsMe_ → 我发的；没有 → 对方发的\n    const isMe = cls.includes('_MsgIsMe_');\n    const sender = isMe ? '【我方/Morois】' : '【对方】';\n\n    // 提取时间\n    const timeEl = el.querySelector('[class*=\"_Msg__Time_\"]');\n    const time = timeEl ? timeEl.textContent.trim() : '';\n\n    // 提取消息内容（可能有多个 Content class，取非重复内容）\n    const contentEls = el.querySelectorAll('[class*=\"_Msg__Content_\"]');\n    const texts = [];\n    for (const c of contentEls) {\n      const t = (c.textContent || '').trim().replace(/\\s+/g, ' ');\n      // 排除时间格式的内容（如\"2026年3月28日 18:00\"）\n      if (t && !t.match(/^\\d{4}年/)) texts.push(t.slice(0, 200));\n    }\n    const uniqueTexts = [...new Set(texts)];\n    const text = uniqueTexts.join(' | ');\n\n    if (text) results.push({ sender, time, text });\n  }\n  return results;\n}\n```\n\n##### 注意事项\n\n- 保留所有消息类型：文本、视频分享、图片、卡片等，不要因为不是纯文本就丢弃\n- 结合时间戳重建时间线\n- DOM 结构变化时，参考常见问题 → 页面布局变了\n\n---\n\n## 常见问题\n\n### 找不到编辑器\n\nB站私信页面的编辑器可能出现在不同位置。尝试：\n\n```javascript\n() => {\n  const selectors = [\n    '.content-input',\n    '[contenteditable=\"true\"]',\n    '[class*=\"editor\"]',\n    '[class*=\"input\"]'\n  ];\n  for (const sel of selectors) {\n    const el = document.querySelector(sel);\n    if (el) return sel + ' found: ' + el.className;\n  }\n  return 'no editor found';\n}\n```\n\n### 找不到发送按钮\n\n发送按钮可能没有稳定的 class 或 ref。查找包含\"发送\"文字的按钮：\n\n```javascript\n() => {\n  const btns = document.querySelectorAll('button, [class*=\"btn\"], [class*=\"send\"]');\n  for (const btn of btns) {\n    if ((btn.textContent || '').includes('发送')) {\n      return 'found: ' + btn.className;\n    }\n  }\n  return 'send button not found';\n}\n```\n\n### 页面布局变了（DOM 结构诊断）\n\n如果脚本返回空结果或找不到元素，按以下顺序诊断：\n\n1. **先快照**：用 `browser action=snapshot` 获取当前页面完整 DOM\n2. **找消息容器**：在快照中搜索 `_Msg_` 或 `message`\n3. **找编辑器**：搜索 `contenteditable`、`input`、`editor`\n4. **找发送按钮**：搜索 `发送` 文字或 `button`\n5. **根据诊断结果**：用实际找到的 class/name 重写选择器\n\n诊断脚本：\n```javascript\n() => {\n  return {\n    msgs: Array.from(document.querySelectorAll('[class*=\"Msg\"]')).slice(0,3).map(el=>el.className),\n    editor: document.querySelector('[contenteditable=\"true\"]') ? 'found' : 'not found',\n    btns: Array.from(document.querySelectorAll('button')).map(b=>b.textContent.trim()).filter(t=>t)\n  };\n}\n```\n\n---\n\n## 限制与注意事项\n\n- 消息长度限制 500 字，超长需分段发送\n- 避免高频发送\n- 读取记录时优先保证归属和类型正确，再追求历史深度\n- 发送前必须用户明确确认目标账号和消息内容\n\nFile v1.4.0:_meta.json\n\n{\n  \"ownerId\": \"kn75k53tcdhqwka2fra5x817p9821zpb\",\n  \"slug\": \"bilibili-messager\",\n  \"version\": \"1.4.0\",\n  \"publishedAt\": 1776005042637\n}","readmeExcerpt":"Skill: Bilibili Messager | B站私信助手 Owner: moroiser Summary: Send Bilibili DMs, reply to messages, read chat history via browser automation. | 通过浏览器自动化发送 B 站私信、回复消息、读私信、看聊天记录。 Tags: latest:1.4.8 Version history: v1.4.8 | 2026-05-04T18:13:16.239Z | user 1. Fixed: _meta.json version consistency with registry. 修复：_meta.json 版本号与 registry 一致。 v1.4.7 | 2026-05-04T18:05:43.979Z | user 1. Fixed: _meta.json version consistency","codeSnippets":[],"executableExamples":[{"language":"text","snippet":"browser action=open targetUrl=https://message.bilibili.com/#/whisper"},{"language":"javascript","snippet":"() => {\n  var editor = document.querySelector('.content-input, [contenteditable=\"true\"]');\n  if (!editor) return 'editor not found';\n  editor.focus();\n  editor.innerText = '消息内容';\n  editor.dispatchEvent(new Event('input', {bubbles: true}));\n  return 'typed';\n}"},{"language":"javascript","snippet":"() => {\n  var btns = document.querySelectorAll('button, [class*=\"btn\"]');\n  for (var btn of btns) {\n    if ((btn.textContent || '').includes('发送')) {\n      btn.click();\n      return 'sent';\n    }\n  }\n  return 'send button not found';\n}"},{"language":"text","snippet":"browser action=open targetUrl=https://message.bilibili.com/#/whisper"},{"language":"text","snippet":"class=\"_Msg_o7f0t_1 _MsgIsMe_o7f0t_9\""},{"language":"text","snippet":"class=\"_Msg_o7f0t_1\""}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: Bilibili Messager | B站私信助手\ndescription: |\n  Send Bilibili DMs, reply to messages, read chat history via browser automation. | 通过浏览器自动化发送 B 站私信、回复消息、读私信、看聊天记录。\nmetadata:\n  \"openclaw\":\n    requires:\n      bins: [\"browser\"]\n      browser_profiles: [\"openclaw\"]  # 必须使用 openclaw profile，且已登录 Bilibili\n    runtime:\n      credentialExpectations:\n        - \"Bilibili 已登录的浏览器会话（openclaw profile）\"\n      filesystemWrites:\n        - \"~/.openclaw/workspace/skills/bilibili-messager/\"\n        - \"~/.openclaw/workspace/projects/bilibili-messager/\"\n    warning:\n      - \"发送操作需要用户两步验证确认\"\n      - \"读取聊天记录会将私信内容暴露到 agent 上下文，仅读取你愿意分享的内容\"\n      - \"使用独立 openclaw profile，不与其他账号混用\"\n---\n\n# Bilibili Messager | B站私信助手\n\n## ⚠️ 安全声明\n\n本技能通过已登录的 Bilibili 浏览器会话读写私信。\n\n**限制：**\n- 只使用独立的 `openclaw` profile（不混用其他账号）\n- 发送前必须两步验证确认目标账号和消息内容\n- 读取模式仅用于你愿意暴露给 agent 的会话内容\n- 不要用于读取高度敏感或非自愿的私人对话\n\n## 前置要求 | Prerequisites\n\n| 条件 | 说明 |\n|------|------|\n| **Browser profile** | 必须使用 `openclaw` profile（已通过 `browser` 工具启动并登录 Bilibili） |\n| **登录状态** | Bilibili 账号已在 `openclaw` profile 中保持登录态 |\n| **账号确认** | 发送操作前，技能会要求用户明确确认目标账号和消息内容 |\n\n### 为什么需要 openclaw profile？\n\n本技能通过在 Bilibili 私信页面执行 DOM JavaScript（`querySelector`、`innerText`、`click` 等）实现消息读取和发送。这些操作需要浏览器环境持有有效的 Bilibili 登录会话。\n\n- `openclaw` profile 由 OpenClaw 启动和管理，独立于 Morois 的日常浏览器 profile\n- 技能仅在 Bilibili 私信页（`message.bilibili.com`）域内执行 DOM 操作，无法访问其他网站\n- 任何对外发送操作均需用户明确确认，不会自动发送\n\n---\n\n## 模式 | Mode\n\n> 读取本 SKILL.md 后，先确认当前任务属于哪种模式，再跳转对应章节执行。\n\n| 模式 | 说明 |\n|------|------|\n| **send** | 发送新私信（必须两步验证） |\n| **reply** | 回复现有会话中的消息（必须两步验证） |\n| **read** | 读取聊天记录（只读，不写入任何内容） |\n\n---\n\n## 通用原则 | General Principles\n\n1. **Browser profile**：使用 `openclaw` profile（已登录 Bilibili）\n2. **两步验证**：发送类操作（send / reply）必须两步验证，不可跳过\n3. **只读优先**：读取聊天记录使用 read 模式，不执行任何写入操作\n4. **发送前确认**：执行任何对外发送前，必须先向用户确认目标账号和消息内容\n---\n\n## 发送私信 | Mode: Send\n\n### 步骤 1：打开私信页并确认登录\n\n```\nbrowser action=open targetUrl=https://message.bilibili.com/#/whisper\n```\n\n检查页面是否跳转到登录页。若跳转到登录页，停下来让用户完成登录。\n\n### 步骤 2：找到目标会话\n\n在左侧会话列表中找到目标联系人。可以通过文字搜索或滚动列表。\n\n- 使用 `browser action=act evaluate` 配合 DOM 查询找到目标会话\n- 找到后点击进入会话\n\n### 步骤 3：两步验证（强制，必须执行）\n\n#### 第一步（AI 内部执行，不输出给用户）\n\n- 确认已正确进入目标会话（检查 URL 包含 `mid` 参数或页面标题显示对方名字）\n- 确认消息内容已准备完毕\n- 检查消息长度（不超过 500 字）\n\n#### 第二步（输出给用户，等待明确确认）\n\n向用户汇报以下全部内容，**确认前不得写入或发送**：\n\n| 汇报项 | 内容 |\n|--------|------|\n| 目标账号 | 会话对方的名字 |\n| 消息内容 | 将要发送的完整内容 |\n| 字数 | 是否在 500 字以内 |\n\n**确认标志**：用户明确回复「好」「确认」「发」「发吧」「发送」等。\n\n### 步骤 4：写入消息\n\n使用 `innerText` 方式写入（经验证有效）：\n\n```javascript\n() => {\n  var editor = document.querySelector('.content-input, [contenteditable=\"true\"]');\n  if (!editor) return 'editor not found';\n  editor.focus();\n  editor.innerText = '消息内容';\n  editor.dispatchEvent(new Event('input', {bubbles: true}));\n  return 'typed';\n}\n```\n\n### 步骤 5：点击发送按钮\n\n```javascript\n() => {\n  var btns = document.querySelectorAll('button, [class*=\"btn\"]');\n  for (var btn of btns) {\n    if ((btn.textContent || '').includes('发送')) {\n      btn.click();\n      return 'sent';\n    }\n  }\n  return 'send button not found';\n}\n```\n\n**注意**："},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn75k53tcdhqwka2fra5x817p9821zpb\",\n  \"slug\": \"bilibili-messager\",\n  \"version\": \"1.4.8\",\n  \"publishedAt\": 1777918396239\n}"},{"path":"skill-card.md","content":"## Description:\n\nSends Bilibili DMs, replies to messages, and reads chat history through a logged-in browser session.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[moroiser](https://clawhub.ai/user/moroiser)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nExternal users who manage Bilibili conversations use this skill to read selected private-message history and draft, send, or reply to DMs after explicit confirmation.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The agent may read selected Bilibili private-message content into its context.\n\nMitigation: Only read conversations the user is willing to share, avoid highly sensitive or non-consensual private messages, and use a dedicated openclaw browser profile.\n\nRisk: The skill can send DMs from the logged-in Bilibili account.\n\nMitigation: Confirm the logged-in account, recipient, message body, and message length before sending or replying; require explicit user confirmation.\n\nRisk: A shared browser profile could expose the wrong account or unrelated browsing context.\n\nMitigation: Use a dedicated openclaw browser profile and clear the Bilibili login state when the skill is no longer needed.\n\n## Reference(s):\n\n- [ClawHub skill page](https://clawhub.ai/moroiser/skills/bilibili-messager)\n- [Publisher profile](https://clawhub.ai/user/moroiser)\n- [Bilibili private messages](https://message.bilibili.com/#/whisper)\n\n## Skill Output:\n\n**Output Type(s):** [Guidance, Shell commands, Code]\n\n**Output Format:** [Markdown guidance with browser tool commands and JavaScript snippets]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Uses a logged-in openclaw browser profile; outgoing messages require explicit recipient and message confirmation.]\n\n## Skill Version(s):\n\n1.4.8 (source: server release evidence and artifact _meta.json)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment."}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":"Send Bilibili DMs, reply to messages, read chat history via browser automation. | 通过浏览器自动化发送 B 站私信、回复消息、读私信、看聊天记录。 Skill: Bilibili Messager | B站私信助手 Owner: moroiser Summary: Send Bilibili DMs, reply to messages, read chat history via browser automation. | 通过浏览器自动化发送 B 站私信、回复消息、读私信、看聊天记录。 Tags: latest:1.4.8 Version history: v1.4.8 | 2026-05-04T18:13:16.239Z | user 1. Fixed: _meta.json version consistency with registry. 修复：_meta.json 版本号与 registry 一致。 v1.4.7 | 2026-05-04T18:05:43.979Z | user 1. Fixed: _meta.json version consistency","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":862,"uniquenessScore":53,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-09T23:06:14.082Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-09T23:06:14.082Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-10T05:40:04.283Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}