{"id":"0282b12a-b0da-47de-ac68-87fb802a8229","entityType":"agent","slug":"clawhub-moroiser-douyin-messager","name":"Douyin Messager | 抖音私信助手","canonicalUrl":"https://www.xpersona.co/agent/clawhub-moroiser-douyin-messager","canonicalPath":"/agent/clawhub-moroiser-douyin-messager","generatedAt":"2026-10-09T17:31:45.262Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-10-09T04:43:38.198Z","emptyReason":null},"description":"Douyin DMs and video/note comment assistant. 抖音私信与视频/图文评论助手；可读私信、分析评论区，评论/回复等写入前必须确认。 Skill: Douyin Messager | 抖音私信助手 Owner: moroiser Summary: Douyin DMs and video/note comment assistant. 抖音私信与视频/图文评论助手；可读私信、分析评论区，评论/回复等写入前必须确认。 Tags: latest:1.2.0 Version history: v1.2.0 | 2026-05-04T17:25:39.073Z | user 1. Added: credentialExpectations, filesystemWrites, and warning metadata for ClawScan. 新增：凭据声明、文件写入范围和警告元数据，适配 ClawScan。 v1.1.9 | 2026-05-04T17:05:59.131Z | user 1. Added: Browser-session credential d","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 4.9K downloads reported by the source. Last updated 10/9/2026.","installCommand":"clawhub skill install s1708w2t7w31qf7b0gsy7kamnx83gqa6:douyin-messager","sourceUrl":"https://clawhub.ai/moroiser/douyin-messager","homepage":"https://clawhub.ai/moroiser/skills/douyin-messager","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/moroiser/douyin-messager","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/moroiser/skills/douyin-messager","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":68,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Douyin DMs and video/note comment assistant. 抖音私信与视频/图文评论助手；可读私信、分析评论区，评论/回复等写入前必须确认。 Skill: Douyin Messager | 抖音私信助手 Owner: moroiser Summary: Douyin DMs and vi"},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-09T04:43:38.198Z","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-09T04:43:38.198Z","emptyReason":null},"stars":null,"forks":null,"downloads":4858,"packageName":null,"latestVersion":"1.2.0","tractionLabel":"4.9K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T04:43:38.197Z","emptyReason":null},"lastUpdatedAt":"2026-10-09T04:43:38.198Z","lastCrawledAt":"2026-10-09T04:43:38.197Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-10T04:43:38.197Z","lastVerifiedAt":null,"highlights":[{"version":"1.2.0","createdAt":"2026-05-04T17:25:39.073Z","changelog":"1. Added: credentialExpectations, filesystemWrites, and warning metadata for ClawScan. 新增：凭据声明、文件写入范围和警告元数据，适配 ClawScan。","fileCount":3,"zipByteSize":9787},{"version":"1.1.9","createdAt":"2026-05-04T17:05:59.131Z","changelog":"1. Added: Browser-session credential declaration with account verification and cleanup steps. 新增：浏览器会话凭据声明，含账户确认和使用后清理步骤。 2. Added: Data disclosure for read-mode context exposure. 新增：读取模式上下文暴露的数据披露。 3. Restructured: Security declaration with credential, data, and operation sections. 重构：安全声明分为凭据、数据、操作限制三个部分。","fileCount":2,"zipByteSize":8313},{"version":"1.1.8","createdAt":"2026-04-30T01:50:36.464Z","changelog":"1. Shorten the bilingual summary so both English and Chinese appear in ClawHub preview cards. 缩短双语简介，确保 ClawHub 预览卡片同时显示英文和中文。","fileCount":2,"zipByteSize":7684},{"version":"1.1.7","createdAt":"2026-04-30T01:46:13.606Z","changelog":"1. Add verified Douyin video/note comment workflows, including modal_id handling, comment reading, sentiment brief guidance, and explicit confirmation rules for external interactions. 新增已验证的抖音视频/图文评论区流程，包括 modal_id 处理、评论读取、情绪简报指引，以及外部互动写入的明确确认规则。","fileCount":2,"zipByteSize":7771},{"version":"1.1.6","createdAt":"2026-04-29T16:36:24.831Z","changelog":"1. Fix H1 title to match frontmatter: Douyin Messager | 抖音私信助手. 2. Add Prerequisites section declaring openclaw browser profile requirement and security scope. 修复标题与 frontmatter 不一致，增加前置要求章节。","fileCount":2,"zipByteSize":3335},{"version":"1.1.5","createdAt":"2026-04-06T13:11:45.579Z","changelog":"1. Fix display name to proper format: Douyin Messager | 抖音私信助手. 修复显示名称为正确格式。","fileCount":2,"zipByteSize":2831},{"version":"1.1.4","createdAt":"2026-04-06T03:33:06.776Z","changelog":"Fix subprocess.run(shell=True) in record_audio.py for security compliance. 修复 record_audio.py 中 subprocess.run(shell=True) 调用，提升安全性。","fileCount":2,"zipByteSize":2825},{"version":"1.1.3","createdAt":"2026-03-24T09:20:19.250Z","changelog":"Improve cross-platform behavior with automatic Windows/macOS detection. 优化跨平台行为，新增 Windows/macOS 自动识别。 Make DM panel navigation more reliable with JS-based targeting. 用 JS 定位提升 DM 面板导航稳定性。 Fix Draft.js input by switching to clipboard paste + Enter dispatch. 通过剪贴板粘贴 + Enter 派发修复 Draft.js 输入兼容性。 Support both pinned-contact and search-based messaging flows. 同时支持置顶联系人与搜索用户两种发信流程。 Add full end-to-end examples for both methods. 新增两套完整可跑通的端到端示例。 Clean up outdated footer metadata. 清理过时的页脚元数据。","fileCount":2,"zipByteSize":2825}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s1708w2t7w31qf7b0gsy7kamnx83gqa6:douyin-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-douyin-messager/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-moroiser-douyin-messager/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-moroiser-douyin-messager/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-moroiser-douyin-messager/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-moroiser-douyin-messager/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-moroiser-douyin-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-09T17:31:45.260Z"}},"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-douyin-messager/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-moroiser-douyin-messager/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-moroiser-douyin-messager/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-moroiser-douyin-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-09T04:43:38.198Z","emptyReason":null},"readme":"Skill: Douyin Messager | 抖音私信助手\n\nOwner: moroiser\n\nSummary: Douyin DMs and video/note comment assistant. 抖音私信与视频/图文评论助手；可读私信、分析评论区，评论/回复等写入前必须确认。\n\nTags: latest:1.2.0\n\nVersion history:\n\nv1.2.0 | 2026-05-04T17:25:39.073Z | user\n\n1. Added: credentialExpectations, filesystemWrites, and warning metadata for ClawScan. 新增：凭据声明、文件写入范围和警告元数据，适配 ClawScan。\n\nv1.1.9 | 2026-05-04T17:05:59.131Z | user\n\n1. Added: Browser-session credential declaration with account verification and cleanup steps. 新增：浏览器会话凭据声明，含账户确认和使用后清理步骤。 2. Added: Data disclosure for read-mode context exposure. 新增：读取模式上下文暴露的数据披露。 3. Restructured: Security declaration with credential, data, and operation sections. 重构：安全声明分为凭据、数据、操作限制三个部分。\n\nv1.1.8 | 2026-04-30T01:50:36.464Z | user\n\n1. Shorten the bilingual summary so both English and Chinese appear in ClawHub preview cards. 缩短双语简介，确保 ClawHub 预览卡片同时显示英文和中文。\n\nv1.1.7 | 2026-04-30T01:46:13.606Z | user\n\n1. Add verified Douyin video/note comment workflows, including modal_id handling, comment reading, sentiment brief guidance, and explicit confirmation rules for external interactions. 新增已验证的抖音视频/图文评论区流程，包括 modal_id 处理、评论读取、情绪简报指引，以及外部互动写入的明确确认规则。\n\nv1.1.6 | 2026-04-29T16:36:24.831Z | user\n\n1. Fix H1 title to match frontmatter: Douyin Messager | 抖音私信助手. 2. Add Prerequisites section declaring openclaw browser profile requirement and security scope. 修复标题与 frontmatter 不一致，增加前置要求章节。\n\nv1.1.5 | 2026-04-06T13:11:45.579Z | user\n\n1. Fix display name to proper format: Douyin Messager | 抖音私信助手. 修复显示名称为正确格式。\n\nv1.1.4 | 2026-04-06T03:33:06.776Z | user\n\nFix subprocess.run(shell=True) in record_audio.py for security compliance. 修复 record_audio.py 中 subprocess.run(shell=True) 调用，提升安全性。\n\nv1.1.3 | 2026-03-24T09:20:19.250Z | user\n\nImprove cross-platform behavior with automatic Windows/macOS detection. 优化跨平台行为，新增 Windows/macOS 自动识别。\nMake DM panel navigation more reliable with JS-based targeting. 用 JS 定位提升 DM 面板导航稳定性。\nFix Draft.js input by switching to clipboard paste + Enter dispatch. 通过剪贴板粘贴 + Enter 派发修复 Draft.js 输入兼容性。\nSupport both pinned-contact and search-based messaging flows. 同时支持置顶联系人与搜索用户两种发信流程。\nAdd full end-to-end examples for both methods. 新增两套完整可跑通的端到端示例。\nClean up outdated footer metadata. 清理过时的页脚元数据。\n\nv1.1.2 | 2026-03-12T18:24:40.568Z | user\n\nRefined skill wording and publishing readiness. 优化技能表述并整理为更适合发布的版本。\n\nv1.1.1 | 2026-03-02T19:55:26.133Z | user\n\nOptimized triggers for higher sensitivity: added douyin dm, message, chat, history keywords. 优化激活词提高灵敏度：增加 douyin dm、message、chat、history 等关键词。\n\nv1.1.0 | 2026-03-02T14:55:38.066Z | user\n\nStandardize name and description format\n\nv1.0.0 | 2026-03-02T05:16:09.348Z | user\n\n首个版本：支持抖音私信发送、获取聊天记录、处理 xdg-open 弹窗问题\n\nArchive index:\n\nArchive v1.2.0: 3 files, 9787 bytes\n\nFiles: _meta.json (134b), skill-card.md (2434b), SKILL.md (24025b)\n\nFile v1.2.0:SKILL.md\n\n---\nname: Douyin Messager | 抖音私信助手\ndescription: \"Douyin DMs and video/note comment assistant. 抖音私信与视频/图文评论助手；可读私信、分析评论区，评论/回复等写入前必须确认。\"\nmetadata:\n  \"openclaw\":\n    requires:\n      bins: [\"browser\"]\n      browser_profiles: [\"openclaw\"]\n    runtime:\n      credentialExpectations:\n        - \"抖音已登录的浏览器会话（openclaw profile）\"\n      filesystemWrites:\n        - \"~/.openclaw/workspace/skills/douyin-messager/\"\n        - \"~/.openclaw/workspace/projects/douyin-messager/\"\n    warning:\n      - \"发送/评论/回复/点赞等写入操作需要用户确认\"\n      - \"读取聊天记录会将私信内容暴露到 agent 上下文\"\n      - \"使用独立 openclaw profile，不与其他账号混用\"\n---\n\n# Douyin Messager | 抖音私信助手\n\n通过浏览器自动化发送抖音私信、获取聊天记录；也支持打开抖音视频/图文链接、读取评论区并做简要分析，在用户明确确认后执行评论/回复等写入操作。\n\n## 前置要求 | Prerequisites\n\n| 条件 | 说明 |\n|------|------|\n| **Browser profile** | 必须使用 `openclaw` profile |\n| **登录状态** | 抖音账号已在 `openclaw` profile 中保持登录态 |\n\n---\n\n## ⚠️ 执行前必须确认\n\n1. **用户已登录抖音账号**（可目测判断）\n2. **xdg-open 弹窗**：只在 Linux 下存在\n   - Linux：询问用户弹窗是否关闭\n   - Windows/macOS：跳过\n\n---\n\n## ⚠️ 安全声明 | Security Notice\n\n本技能通过已登录的抖音浏览器会话读写私信和评论。\n\n### 浏览器会话凭据声明\n\n本技能依赖已登录的抖音浏览器会话作为账户凭据。使用前请确认：\n\n1. **确认登录账户**：发送操作前，先确认当前登录的是正确的抖音账户\n2. **使用专用 profile**：必须使用独立的 `openclaw` profile，不与日常浏览器混用\n3. **用完后清理**：关闭 `openclaw` profile 的浏览器标签页即可；不再使用时，可清除该 profile 的抖音登录态\n\n### 数据披露\n\n- 读取聊天记录和评论区会将内容暴露到 agent 上下文，仅读取你愿意分享的内容\n- 不要用于读取高度敏感或非自愿的私人对话\n\n### 操作限制\n\n- **发送私信**：必须先向用户确认目标账号和消息内容，获得明确同意后才执行\n- **评论/回复/点赞/分享**：属于外部互动写入操作，必须先获得用户明确确认\n\n---\n\n## 当前能力边界（阶段性总结）\n\n本技能当前已验证可完成四类操作：\n\n1. **查看私信会话列表**：进入私信悬浮面板后，读取可见的私聊/群聊列表，区分会话名称、最新消息预览、时间、未读数和置顶状态。\n2. **进入具体聊天窗口**：从会话列表点击指定私聊或群聊，进入聊天详情页，读取当前已加载的聊天记录。\n3. **发送文本消息**：在聊天详情底部的 Draft.js 输入框中输入文本，并通过发送按钮完成发送。\n4. **视频/图文评论区处理**：可搜索或打开指定视频/图文链接，读取评论区，输出情绪简报；评论、回复、点赞等外部互动写入必须先获得用户明确确认。\n\n需要注意：网页版对视频、图集、点赞、撤回、暂不支持消息等卡片类内容的 DOM 暴露不完整，读取时应保守标注，不应强行推断完整内容或发送者。\n\n---\n\n## 核心思路\n\n> 抖音私信是一个**悬浮在页面右侧的动态面板**，包含\"会话列表\"和\"聊天详情\"两个视图。\n\n**会话列表布局**（类 QQ/微信）：左侧头像，中间分上下两行（上行：用户名/群名；下行：最新消息预览，群聊格式为「发消息的人：消息内容」），右侧时间戳。\n\n**聊天详情布局**：进入某个会话后，右侧显示聊天记录。普通文本消息通常可提取时间、消息内容，并可根据气泡方向、操作项（如是否有「撤回」）辅助判断是否为本人消息；群聊中他人文本消息有时可从消息文本或引用结构中识别发送者。卡片类消息需谨慎处理。\n\n**方法论**：不依赖固定 class name，而是通过元素的**几何特征 + 内容特征**动态查找。具体 class name 仅作参考示例。\n\n---\n\n## 获取私信列表流程\n\n获取私信的第一步不是进入某个聊天，而是**正确进入私信会话列表**。\n\n### 步骤 1：打开私信会话列表\n\n1. 打开抖音主页或个人主页。\n2. 在顶部导航栏右侧定位「私信」入口（可能显示未读数，如「私信11」）。\n3. 点击后等待右侧悬浮面板出现。\n4. 确认当前处于会话列表视图，而不是某个聊天详情页。\n\n### 步骤 2：识别每一行会话\n\n会话列表中每一行对应一个私聊或群聊。布局通常为：左侧头像；中间上行为会话名称（用户名/群名，可能带「置顶」标记）；中间下行为最新消息预览；右侧为时间戳或未读标记。\n\n⚠️ 会话列表本身是可滚动容器，不要只按当前可见区域判断数量。实测中滚动容器宽约 328px、高约 593px，`scrollHeight > clientHeight`，可通过 `scrollTop` 滚到底部；出现「暂时没有更多了」才表示到底。\n\n解析时应优先按**行容器**处理，而不是直接读取父元素的合并文本。读取每行时分别提取：\n\n| 字段 | 识别方法 |\n|------|----------|\n| 会话名称 | 行内上方文本，通常是用户名或群名；若有「置顶」应作为标记处理，不并入名称 |\n| 最新消息 | 行内下方文本；群聊常见格式为「发送者：消息内容」 |\n| 最新发送者 | 若最新消息包含「：」，冒号前通常是群聊中最新消息发送者 |\n| 时间 | 行右侧或下行末尾的时间文本，如「昨天」「04-25」「01:42」 |\n| 未读数 | 行右侧 badge 数字；顶部「私信（N）」通常表示总未读数 |\n| 置顶 | 名称行附近出现「置顶」标记时记录为 pinned=true |\n\n### 步骤 3：区分私聊与群聊\n\n- 私聊：上行通常是对方昵称；下行多为消息内容本身。\n- 群聊：上行是群名；下行常为「发送者：消息内容」。\n- 不要把上行群名和下行发送者合并为一个名称。\n\n**示例参考**：\n\n| 上行（会话名称） | 下行（最新消息） | 解析结果 |\n|------------------|------------------|----------|\n| `示例群A` | `成员甲：分享[视频] · 昨天` | 群名/会话名=`示例群A`，最新发送者=`成员甲` |\n| `项目交流群` | `成员乙：分享[图集] · 04-25` | 群名=`项目交流群`，最新发送者=`成员乙` |\n| `联系人A` + `置顶` | `收到，谢谢 · 01:42` | 私聊=`联系人A`，pinned=true |\n\n### 示例：提取可见会话行文本\n\n以下代码用于辅助观察当前可见列表，不应把 class name 当成稳定依赖：\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const rows = []; const all = Array.from(document.querySelectorAll('*')); for (const el of all) { const r = el.getBoundingClientRect(); const text = (el.textContent || '').trim(); if (r.width >= 250 && r.width <= 360 && r.height >= 50 && r.height <= 90 && r.left > window.innerWidth * 0.55 && text && !text.includes('私信（')) { rows.push({top: Math.round(r.top), text}); } } return rows.sort((a,b) => a.top - b.top); }\"}\n```\n\n---\n\n## 完整发送流程\n\n### 步骤 1：打开抖音主页\n\n```javascript\nbrowser action=open profile=openclaw targetUrl=https://www.douyin.com/\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 3000}\n```\n\n### 步骤 2：打开私信悬浮面板\n\n**找按钮**：在页面顶部导航栏中，找到文字为\"私信\"（可能带未读数如\"私信15\"）且位于页面最右侧的元素，点击它。\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const t = (el.textContent||'').trim(); const r = el.getBoundingClientRect(); if((t === '私信' || t.startsWith('私信')) && r.width > 0 && r.height > 0 && r.width < 100 && r.left > window.innerWidth - 200) { el.click(); return 'clicked: ' + t; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n```\n\n**找面板**：点击后，页面右侧应出现一个约 500-600px 宽、600-700px 高的悬浮面板。\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = document.querySelectorAll('[class]'); for(const el of all) { const r = el.getBoundingClientRect(); if(r.width >= 450 && r.width <= 650 && r.height >= 500 && r.height <= 750 && r.left > 0 && r.top > 0 && r.left < 1500) { const text = (el.textContent||'').trim(); if(text.includes('私信')) return 'found panel: ' + r.width + 'x' + r.height + ' at ' + Math.round(r.left); } } return 'panel not found'; }\"}\n```\n\n> **示例参考**：实测中面板容器 class 为 `vgonMAXk`，宽 581px、高 649px，固定在 x:1235 位置。但这是动态的，应以几何定位为主。\n\n### 步骤 3：进入具体聊天\n\n**找目标用户**：在面板的会话列表中，找到目标用户名的元素并点击。\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const t = (el.textContent||'').trim(); const r = el.getBoundingClientRect(); if(t.includes('<目标用户名>') && r.width > 0 && r.height > 0 && r.left > 0) { el.click(); return 'clicked: ' + t; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n```\n\n**成功标志**：面板从会话列表切换为聊天详情，底部出现输入区域。\n\n> **示例参考**：会话列表区域 class 为 `zEpd_aAP`（左侧，500px宽）；聊天详情区域 class 为 `w5duGc5Q`（右侧）。\n\n### 步骤 4：两步验证（强制，必须执行）\n\n#### 第一步（AI 内部执行，不输出给用户）\n\n- 确认已正确进入目标会话（检查页面显示对方名字）\n- 确认消息内容已准备完毕\n- 检查消息长度\n\n#### 第二步（输出给用户，等待明确确认）\n\n向用户汇报以下全部内容，**确认前不得写入或发送**：\n\n| 汇报项 | 内容 |\n|--------|------|\n| 目标账号 | 会话对方的名字 |\n| 消息内容 | 将要发送的完整内容 |\n| 字数 | 是否超长 |\n\n**确认标志**：用户明确回复「好」「确认」「发」「发吧」「发送」等。\n\n### 步骤 5：写入消息\n\n**找输入框**：聊天详情底部有一个 Draft.js 富文本编辑器（`contenteditable=\"true\"`，class 包含 `DraftEditor-content`）。\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = document.querySelectorAll('[contenteditable=\\\"true\\\"]'); for(const el of all) { const r = el.getBoundingClientRect(); if(r.width > 200 && r.width < 500 && r.height > 10 && r.height < 100 && r.left > 1000) return 'found input at ' + Math.round(r.left) + ',' + Math.round(r.top); } return 'input not found'; }\"}\n```\n\n**发送消息**：先用 `type` 方式写入 Draft.js 编辑器，再点击发送按钮。\n\n```javascript\n// 1. 写入文本\nbrowser action=act request={\"kind\": \"type\", \"selector\": \"[contenteditable=\\\"true\\\"]\", \"text\": \"你好，测试消息\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 500}\n\n// 2. 点击发送按钮（优先用几何位置查找，也可用实测 class 作为参考）\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const btn = Array.from(document.querySelectorAll('*')).find(el => { const r = el.getBoundingClientRect(); const cls = String(el.className || ''); return r.width >= 20 && r.width <= 50 && r.height >= 20 && r.height <= 50 && r.left > window.innerWidth * 0.7 && r.top > window.innerHeight * 0.55 && (cls.includes('send') || cls.includes('PygT7Ced')); }); if(!btn) return 'send button not found'; const r = btn.getBoundingClientRect(); btn.click(); return 'clicked send at ' + Math.round(r.left + r.width/2) + ',' + Math.round(r.top + r.height/2); }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n```\n\n> ⚠️ **必须用 `type` 写入 Draft.js 编辑器**。直接操作 DOM 文本（`textContent`、`execCommand`、`clipboard`）无法触发 Draft.js 内部状态，发送按钮会保持禁用。\n\n> **示例参考**：实测中输入框 class 为 `notranslate public-DraftEditor-content`（`contenteditable=\"true\"`），宽约 329px，位置 x:1346, y:651。\n\n### 步骤 5：确认发送成功\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const input = document.querySelector('[contenteditable=\\\"true\\\"]'); if(!input) return 'input not found'; return input.textContent.length === 0 ? 'sent ✓' : 'not sent: ' + input.textContent; }\"}\n```\n\n**发送成功后**：输入框被自动清空。\n\n---\n\n## 定位方法总结\n\n| 目标 | 定位策略 | 实测示例（参考）|\n|------|----------|----------------|\n| 「私信」按钮 | 文字=\"私信\"或\"私信数字\"，位于页面最右侧（`r.left > innerWidth - 200`）| 文字=\"私信15\"，viewport x:1708, y:28 |\n| 私信面板 | 宽 450-650px、高 500-750px、固定在页面右侧，内容含\"私信\" | class=`vgonMAXk`，581×649px，x:1235 |\n| 会话列表 | 面板左侧区域 | class=`zEpd_aAP`（宽~81px） |\n| 聊天详情 | 面板右侧区域 | class=`w5duGc5Q`（宽~500px） |\n| 输入框 | `contenteditable=\"true\"`，宽 200-500px，位于页面右侧 | class=`notranslate public-DraftEditor-content`，329×22px |\n| 发送按钮 | 输入框右侧；写入文本后点击 | class 示例：`PygT7Ced e2e-send-msg-btn` |\n\n> **注意**：以上尺寸和位置基于典型 1920px 宽屏幕实测。class name 会随抖音版本变化，**几何特征定位是更稳定的方法**。\n\n---\n\n## 读取具体聊天记录\n\n进入某个私聊或群聊后，可以读取聊天详情页中当前已加载的消息块。读取时建议按**消息块容器**处理，而不是直接读取整个聊天面板的合并文本。\n\n### 可识别字段\n\n| 字段 | 说明 |\n|------|------|\n| 时间 | 消息块上方或附近的时间文本，如「刚刚」「2026-04-04 19:07」 |\n| 消息内容 | 普通文本消息可从气泡文本中提取 |\n| 是否本人消息 | 可结合气泡位置、是否出现「撤回」操作等特征辅助判断 |\n| 群聊发送者 | 普通文本或引用消息中可能出现发送者名称；若 DOM 未明确暴露，应标注为未知/推断 |\n| 卡片类内容 | 视频、图集、点赞、撤回、不支持类型等可能只显示占位文本，不应过度解析 |\n\n### 示例：读取当前已加载消息块\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const area = document.querySelector('.IRB0Sra6') || document.querySelector('.z1iI1SFY'); if(!area) return 'message area not found'; const blocks = Array.from(area.querySelectorAll('.mM66nPpS')); return blocks.map(block => { const time = block.querySelector('.mA74174G')?.textContent?.trim() || ''; const text = (block.querySelector('.G3hOMUUp') || block.querySelector('.J3X6BOUb') || block).textContent.trim(); const mine = block.textContent.includes('撤回'); return { time, mine, text }; }); }\"}\n```\n\n> 上述 class name 是实测示例；正式逻辑应优先用消息区域的几何位置和消息块尺寸筛选，再结合文本特征解析。\n\n---\n\n## 获取对方回复\n\n### 滚动到最新消息\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const r = el.getBoundingClientRect(); if(r.width >= 400 && r.width <= 600 && r.height >= 300 && r.height <= 800 && r.left > 800) { el.scrollTop = el.scrollHeight; return 'scrolled'; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 1000}\n```\n\n### 截图确认\n\n```\nbrowser action=screenshot\n```\n\n---\n\n## 视频搜索与评论区操作（阶段性验证）\n\n当前已验证可完成以下只读/可定位操作：\n\n1. **按关键词搜索视频**：可直接打开 `https://www.douyin.com/search/<关键词>?type=video`，从搜索结果中提取 `/video/<id>` 链接、标题、作者、时间和互动数字。\n2. **打开指定视频/图文**：可直接导航到 `https://www.douyin.com/video/<id>` 或用户给出的分享链接。图文/笔记类内容可能会规范化跳转为 `/note/<id>`，个人页弹窗链接中的 `modal_id=<id>` 也可作为目标内容 ID 使用。\n3. **读取视频信息**：可从页面文本中读取标题、作者、发布时间、点赞/评论/收藏/分享等可见数字（具体字段需按页面布局解析）。\n4. **读取评论区**：视频页评论区在页面下方，需滚动到「全部评论」区域；可读取可见评论的昵称、内容、时间/地区、点赞数、分享/回复入口，并可继续向下滚动加载更多评论。\n5. **定位发评论输入框**：点击「留下你的精彩评论吧」后，会出现 Draft.js 输入框：`contenteditable=\"true\"` 且 class 包含 `public-DraftEditor-content`。\n6. **定位回复输入框**：点击某条评论的「回复」后，会在该评论下方出现 Draft.js 输入框，placeholder 形如「回复@用户名」。\n7. **定位发送按钮**：输入文字后，评论框右侧会出现图标按钮区，最右侧圆形上箭头按钮为发送/发布入口。\n8. **定位评论点赞入口**：每条评论的操作区包含点赞数、分享、回复；点赞图标/数字区域可通过评论块几何位置定位。\n9. **评论区情绪简报**：基于已加载评论文本，按正向/中性/负向/争议或信息不足分类，输出样本量、主要情绪、典型主题和置信度；样本少时必须注明限制。\n\n⚠️ 发评论、回复评论、点赞、分享等都属于外部互动写入操作，必须先获得用户明确确认；测试时可以定位输入框和按钮，但不要擅自提交。\n\n### 搜索并提取视频链接示例\n\n```javascript\nbrowser action=navigate profile=openclaw targetUrl=\"https://www.douyin.com/search/OpenClaw?type=video\"\nbrowser action=act request={\"kind\":\"wait\",\"timeMs\":3000}\nbrowser action=act request={\"kind\":\"evaluate\",\"fn\":\"() => Array.from(document.querySelectorAll('a[href*=\\\"/video/\\\"]')).map(a => ({ href: a.href, text: (a.innerText || a.textContent || '').trim() })).slice(0, 20)\"}\n```\n\n### 打开视频并滚动到评论区\n\n```javascript\nbrowser action=navigate profile=openclaw targetUrl=\"https://www.douyin.com/video/<video_id>\"\nbrowser action=act request={\"kind\":\"wait\",\"timeMs\":5000}\nbrowser action=act request={\"kind\":\"evaluate\",\"fn\":\"() => { const el = Array.from(document.querySelectorAll('*')).find(el => (el.innerText || el.textContent || '').includes('全部评论') && el.getBoundingClientRect().y > 100); if (el) { el.scrollIntoView({block:'start'}); return 'scrolled to comments'; } return 'comments not found'; }\"}\n```\n\n### 从个人页弹窗链接打开目标内容\n\n用户可能发送形如 `https://www.douyin.com/user/self?...&modal_id=<id>&showTab=like` 的链接。此类链接可直接打开；若页面进入个人页弹窗，也可提取 `modal_id` 后直接尝试：\n\n```javascript\nbrowser action=navigate profile=openclaw targetUrl=\"https://www.douyin.com/video/<modal_id>\"\nbrowser action=act request={\"kind\":\"wait\",\"timeMs\":5000}\n// 页面可能自动跳转为 /note/<modal_id>，属于正常情况。\n```\n\n### 读取可见评论示例\n\n```javascript\nbrowser action=act request={\"kind\":\"evaluate\",\"fn\":\"() => Array.from(document.querySelectorAll('*')).map(el => { const r = el.getBoundingClientRect(); const text = (el.innerText || el.textContent || '').trim(); return { r, text, cls: String(el.className || '') }; }).filter(o => o.r.width > 300 && o.r.height > 60 && o.text.includes('回复') && o.text.includes('·')).slice(0, 20).map(o => o.text)\"}\n```\n\n图文/笔记页的评论区可能在右侧栏，通过「评论(N)」标签切换；即使初始显示 `评论(0)`，点击后也可能加载真实评论。读取时以当前 DOM 文本为准，并检查是否出现「暂时没有更多评论」。\n\n### 评论区情绪简报模板\n\n```markdown\n评论区情绪简报：\n- 样本量：已加载 N 条评论\n- 整体情绪：正向 / 中性 / 负向 / 混合 / 信息不足\n- 主要主题：……\n- 风险/争议点：……\n- 置信度：高 / 中 / 低（说明原因）\n```\n\n### 评论/回复输入框定位\n\n```javascript\n// 点击「留下你的精彩评论吧」或某条评论的「回复」后：\nbrowser action=act request={\"kind\":\"evaluate\",\"fn\":\"() => { const input = document.querySelector('[contenteditable=\\\"true\\\"].public-DraftEditor-content'); if (!input) return 'input not found'; const r = input.getBoundingClientRect(); return { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height), text: input.innerText }; }\"}\n```\n\n---\n\n## 常见问题\n\n**Q: 点击「私信」按钮没反应？**\nA: 确认点击的是正确的按钮（顶栏最右侧，带\"私信\"文字）。也可先导航到 `/user/self` 页面再点击。\n\n**Q: 私信面板找不到？**\nA: 确认点击后有等待足够时间（`wait 2000ms`）。面板宽约 500px、高约 650px，固定在页面右侧。\n\n**Q: 输入框找不到？**\nA: 必须先在会话列表中点击一个具体用户，进入聊天详情模式，输入框才会出现。\n\n**Q: 消息输入了但发送不成功？**\nA: 先用 `kind: 'type'` 写入 Draft.js 输入框，再点击发送按钮。直接操作 DOM 文本（`textContent`赋值、`execCommand`、`clipboard paste`）无法触发 Draft.js 状态。\n\n**Q: 怎么确认发送成功了？**\nA: 发送成功后输入框被自动清空（textContent 变为空字符串）。\n\n---\n\n## 完整示例\n\n```javascript\n// 1. 打开抖音\nbrowser action=open profile=openclaw targetUrl=https://www.douyin.com/\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 3000}\n\n// 2. 点击「私信」按钮\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const t = (el.textContent||'').trim(); const r = el.getBoundingClientRect(); if((t === '私信' || t.startsWith('私信')) && r.width > 0 && r.height > 0 && r.width < 100 && r.left > window.innerWidth - 200) { el.click(); return 'clicked: ' + t; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n// 3. 点击目标用户\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const t = (el.textContent||'').trim(); const r = el.getBoundingClientRect(); if(t.includes('<目标用户名>') && r.width > 0 && r.height > 0) { el.click(); return 'clicked: ' + t; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n// 4. 写入消息并点击发送\nbrowser action=act request={\"kind\": \"type\", \"selector\": \"[contenteditable=\\\"true\\\"]\", \"text\": \"你好，这是测试消息\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 500}\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const btn = Array.from(document.querySelectorAll('*')).find(el => { const r = el.getBoundingClientRect(); const cls = String(el.className || ''); return r.width >= 20 && r.width <= 50 && r.height >= 20 && r.height <= 50 && r.left > window.innerWidth * 0.7 && r.top > window.innerHeight * 0.55 && (cls.includes('send') || cls.includes('PygT7Ced')); }); if(!btn) return 'send button not found'; btn.click(); return 'clicked send'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n// 5. 确认发送\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const input = document.querySelector('[contenteditable=\\\"true\\\"]'); return input ? (input.textContent.length === 0 ? 'sent ✓' : 'not sent: ' + input.textContent) : 'input not found'; }\"}\n```\n\nFile v1.2.0:_meta.json\n\n{\n  \"ownerId\": \"kn75k53tcdhqwka2fra5x817p9821zpb\",\n  \"slug\": \"douyin-messager\",\n  \"version\": \"1.2.0\",\n  \"publishedAt\": 1777915539073\n}\n\nFile v1.2.0:skill-card.md\n\n## Description:\n\nA Douyin DM and video/note comment assistant for reading messages and comments, analyzing comment threads, and performing confirmed comments or replies.\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 and agents use this skill to operate Douyin through a logged-in browser session: review DM conversations, read and summarize visible comments, and draft or perform explicitly confirmed messaging and comment interactions.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Broad page selectors may target the wrong recipient, composer, or content under the user's logged-in Douyin account.\n\nMitigation: Before any send, comment, reply, like, or share action, verify the active account, intended recipient or target content, and exact text with the user.\n\nRisk: Reading DMs or comments exposes private or sensitive Douyin content to the agent context.\n\nMitigation: Use the skill only with messages and comment threads the user is comfortable sharing, and avoid highly sensitive or non-consensual private conversations.\n\nRisk: Using a shared browser profile can mix account credentials or activity with unrelated browsing sessions.\n\nMitigation: Use the dedicated openclaw browser profile and confirm the logged-in account before interactive operations.\n\n## Reference(s):\n\n- [ClawHub skill page](https://clawhub.ai/moroiser/skills/douyin-messager)\n- [Publisher profile](https://clawhub.ai/user/moroiser)\n- [Douyin web app](https://www.douyin.com/)\n- [Douyin video search example](https://www.douyin.com/search/OpenClaw?type=video)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, code, shell commands, configuration, guidance]\n\n**Output Format:** [Markdown guidance with browser automation commands and JavaScript snippets]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Requires a logged-in Douyin browser session using the openclaw profile; write actions require explicit user confirmation.]\n\n## Skill Version(s):\n\n1.2.0 (source: server release evidence)\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.1.9: 2 files, 8313 bytes\n\nFiles: _meta.json (134b), SKILL.md (23450b)\n\nFile v1.1.9:SKILL.md\n\n---\nname: Douyin Messager | 抖音私信助手\ndescription: \"Douyin DMs and video/note comment assistant. 抖音私信与视频/图文评论助手；可读私信、分析评论区，评论/回复等写入前必须确认。\"\n---\n\n# Douyin Messager | 抖音私信助手\n\n通过浏览器自动化发送抖音私信、获取聊天记录；也支持打开抖音视频/图文链接、读取评论区并做简要分析，在用户明确确认后执行评论/回复等写入操作。\n\n## 前置要求 | Prerequisites\n\n| 条件 | 说明 |\n|------|------|\n| **Browser profile** | 必须使用 `openclaw` profile |\n| **登录状态** | 抖音账号已在 `openclaw` profile 中保持登录态 |\n\n---\n\n## ⚠️ 执行前必须确认\n\n1. **用户已登录抖音账号**（可目测判断）\n2. **xdg-open 弹窗**：只在 Linux 下存在\n   - Linux：询问用户弹窗是否关闭\n   - Windows/macOS：跳过\n\n---\n\n## ⚠️ 安全声明 | Security Notice\n\n本技能通过已登录的抖音浏览器会话读写私信和评论。\n\n### 浏览器会话凭据声明\n\n本技能依赖已登录的抖音浏览器会话作为账户凭据。使用前请确认：\n\n1. **确认登录账户**：发送操作前，先确认当前登录的是正确的抖音账户\n2. **使用专用 profile**：必须使用独立的 `openclaw` profile，不与日常浏览器混用\n3. **用完后清理**：关闭 `openclaw` profile 的浏览器标签页即可；不再使用时，可清除该 profile 的抖音登录态\n\n### 数据披露\n\n- 读取聊天记录和评论区会将内容暴露到 agent 上下文，仅读取你愿意分享的内容\n- 不要用于读取高度敏感或非自愿的私人对话\n\n### 操作限制\n\n- **发送私信**：必须先向用户确认目标账号和消息内容，获得明确同意后才执行\n- **评论/回复/点赞/分享**：属于外部互动写入操作，必须先获得用户明确确认\n\n---\n\n## 当前能力边界（阶段性总结）\n\n本技能当前已验证可完成四类操作：\n\n1. **查看私信会话列表**：进入私信悬浮面板后，读取可见的私聊/群聊列表，区分会话名称、最新消息预览、时间、未读数和置顶状态。\n2. **进入具体聊天窗口**：从会话列表点击指定私聊或群聊，进入聊天详情页，读取当前已加载的聊天记录。\n3. **发送文本消息**：在聊天详情底部的 Draft.js 输入框中输入文本，并通过发送按钮完成发送。\n4. **视频/图文评论区处理**：可搜索或打开指定视频/图文链接，读取评论区，输出情绪简报；评论、回复、点赞等外部互动写入必须先获得用户明确确认。\n\n需要注意：网页版对视频、图集、点赞、撤回、暂不支持消息等卡片类内容的 DOM 暴露不完整，读取时应保守标注，不应强行推断完整内容或发送者。\n\n---\n\n## 核心思路\n\n> 抖音私信是一个**悬浮在页面右侧的动态面板**，包含\"会话列表\"和\"聊天详情\"两个视图。\n\n**会话列表布局**（类 QQ/微信）：左侧头像，中间分上下两行（上行：用户名/群名；下行：最新消息预览，群聊格式为「发消息的人：消息内容」），右侧时间戳。\n\n**聊天详情布局**：进入某个会话后，右侧显示聊天记录。普通文本消息通常可提取时间、消息内容，并可根据气泡方向、操作项（如是否有「撤回」）辅助判断是否为本人消息；群聊中他人文本消息有时可从消息文本或引用结构中识别发送者。卡片类消息需谨慎处理。\n\n**方法论**：不依赖固定 class name，而是通过元素的**几何特征 + 内容特征**动态查找。具体 class name 仅作参考示例。\n\n---\n\n## 获取私信列表流程\n\n获取私信的第一步不是进入某个聊天，而是**正确进入私信会话列表**。\n\n### 步骤 1：打开私信会话列表\n\n1. 打开抖音主页或个人主页。\n2. 在顶部导航栏右侧定位「私信」入口（可能显示未读数，如「私信11」）。\n3. 点击后等待右侧悬浮面板出现。\n4. 确认当前处于会话列表视图，而不是某个聊天详情页。\n\n### 步骤 2：识别每一行会话\n\n会话列表中每一行对应一个私聊或群聊。布局通常为：左侧头像；中间上行为会话名称（用户名/群名，可能带「置顶」标记）；中间下行为最新消息预览；右侧为时间戳或未读标记。\n\n⚠️ 会话列表本身是可滚动容器，不要只按当前可见区域判断数量。实测中滚动容器宽约 328px、高约 593px，`scrollHeight > clientHeight`，可通过 `scrollTop` 滚到底部；出现「暂时没有更多了」才表示到底。\n\n解析时应优先按**行容器**处理，而不是直接读取父元素的合并文本。读取每行时分别提取：\n\n| 字段 | 识别方法 |\n|------|----------|\n| 会话名称 | 行内上方文本，通常是用户名或群名；若有「置顶」应作为标记处理，不并入名称 |\n| 最新消息 | 行内下方文本；群聊常见格式为「发送者：消息内容」 |\n| 最新发送者 | 若最新消息包含「：」，冒号前通常是群聊中最新消息发送者 |\n| 时间 | 行右侧或下行末尾的时间文本，如「昨天」「04-25」「01:42」 |\n| 未读数 | 行右侧 badge 数字；顶部「私信（N）」通常表示总未读数 |\n| 置顶 | 名称行附近出现「置顶」标记时记录为 pinned=true |\n\n### 步骤 3：区分私聊与群聊\n\n- 私聊：上行通常是对方昵称；下行多为消息内容本身。\n- 群聊：上行是群名；下行常为「发送者：消息内容」。\n- 不要把上行群名和下行发送者合并为一个名称。\n\n**示例参考**：\n\n| 上行（会话名称） | 下行（最新消息） | 解析结果 |\n|------------------|------------------|----------|\n| `示例群A` | `成员甲：分享[视频] · 昨天` | 群名/会话名=`示例群A`，最新发送者=`成员甲` |\n| `项目交流群` | `成员乙：分享[图集] · 04-25` | 群名=`项目交流群`，最新发送者=`成员乙` |\n| `联系人A` + `置顶` | `收到，谢谢 · 01:42` | 私聊=`联系人A`，pinned=true |\n\n### 示例：提取可见会话行文本\n\n以下代码用于辅助观察当前可见列表，不应把 class name 当成稳定依赖：\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const rows = []; const all = Array.from(document.querySelectorAll('*')); for (const el of all) { const r = el.getBoundingClientRect(); const text = (el.textContent || '').trim(); if (r.width >= 250 && r.width <= 360 && r.height >= 50 && r.height <= 90 && r.left > window.innerWidth * 0.55 && text && !text.includes('私信（')) { rows.push({top: Math.round(r.top), text}); } } return rows.sort((a,b) => a.top - b.top); }\"}\n```\n\n---\n\n## 完整发送流程\n\n### 步骤 1：打开抖音主页\n\n```javascript\nbrowser action=open profile=openclaw targetUrl=https://www.douyin.com/\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 3000}\n```\n\n### 步骤 2：打开私信悬浮面板\n\n**找按钮**：在页面顶部导航栏中，找到文字为\"私信\"（可能带未读数如\"私信15\"）且位于页面最右侧的元素，点击它。\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const t = (el.textContent||'').trim(); const r = el.getBoundingClientRect(); if((t === '私信' || t.startsWith('私信')) && r.width > 0 && r.height > 0 && r.width < 100 && r.left > window.innerWidth - 200) { el.click(); return 'clicked: ' + t; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n```\n\n**找面板**：点击后，页面右侧应出现一个约 500-600px 宽、600-700px 高的悬浮面板。\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = document.querySelectorAll('[class]'); for(const el of all) { const r = el.getBoundingClientRect(); if(r.width >= 450 && r.width <= 650 && r.height >= 500 && r.height <= 750 && r.left > 0 && r.top > 0 && r.left < 1500) { const text = (el.textContent||'').trim(); if(text.includes('私信')) return 'found panel: ' + r.width + 'x' + r.height + ' at ' + Math.round(r.left); } } return 'panel not found'; }\"}\n```\n\n> **示例参考**：实测中面板容器 class 为 `vgonMAXk`，宽 581px、高 649px，固定在 x:1235 位置。但这是动态的，应以几何定位为主。\n\n### 步骤 3：进入具体聊天\n\n**找目标用户**：在面板的会话列表中，找到目标用户名的元素并点击。\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const t = (el.textContent||'').trim(); const r = el.getBoundingClientRect(); if(t.includes('<目标用户名>') && r.width > 0 && r.height > 0 && r.left > 0) { el.click(); return 'clicked: ' + t; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n```\n\n**成功标志**：面板从会话列表切换为聊天详情，底部出现输入区域。\n\n> **示例参考**：会话列表区域 class 为 `zEpd_aAP`（左侧，500px宽）；聊天详情区域 class 为 `w5duGc5Q`（右侧）。\n\n### 步骤 4：两步验证（强制，必须执行）\n\n#### 第一步（AI 内部执行，不输出给用户）\n\n- 确认已正确进入目标会话（检查页面显示对方名字）\n- 确认消息内容已准备完毕\n- 检查消息长度\n\n#### 第二步（输出给用户，等待明确确认）\n\n向用户汇报以下全部内容，**确认前不得写入或发送**：\n\n| 汇报项 | 内容 |\n|--------|------|\n| 目标账号 | 会话对方的名字 |\n| 消息内容 | 将要发送的完整内容 |\n| 字数 | 是否超长 |\n\n**确认标志**：用户明确回复「好」「确认」「发」「发吧」「发送」等。\n\n### 步骤 5：写入消息\n\n**找输入框**：聊天详情底部有一个 Draft.js 富文本编辑器（`contenteditable=\"true\"`，class 包含 `DraftEditor-content`）。\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = document.querySelectorAll('[contenteditable=\\\"true\\\"]'); for(const el of all) { const r = el.getBoundingClientRect(); if(r.width > 200 && r.width < 500 && r.height > 10 && r.height < 100 && r.left > 1000) return 'found input at ' + Math.round(r.left) + ',' + Math.round(r.top); } return 'input not found'; }\"}\n```\n\n**发送消息**：先用 `type` 方式写入 Draft.js 编辑器，再点击发送按钮。\n\n```javascript\n// 1. 写入文本\nbrowser action=act request={\"kind\": \"type\", \"selector\": \"[contenteditable=\\\"true\\\"]\", \"text\": \"你好，测试消息\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 500}\n\n// 2. 点击发送按钮（优先用几何位置查找，也可用实测 class 作为参考）\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const btn = Array.from(document.querySelectorAll('*')).find(el => { const r = el.getBoundingClientRect(); const cls = String(el.className || ''); return r.width >= 20 && r.width <= 50 && r.height >= 20 && r.height <= 50 && r.left > window.innerWidth * 0.7 && r.top > window.innerHeight * 0.55 && (cls.includes('send') || cls.includes('PygT7Ced')); }); if(!btn) return 'send button not found'; const r = btn.getBoundingClientRect(); btn.click(); return 'clicked send at ' + Math.round(r.left + r.width/2) + ',' + Math.round(r.top + r.height/2); }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n```\n\n> ⚠️ **必须用 `type` 写入 Draft.js 编辑器**。直接操作 DOM 文本（`textContent`、`execCommand`、`clipboard`）无法触发 Draft.js 内部状态，发送按钮会保持禁用。\n\n> **示例参考**：实测中输入框 class 为 `notranslate public-DraftEditor-content`（`contenteditable=\"true\"`），宽约 329px，位置 x:1346, y:651。\n\n### 步骤 5：确认发送成功\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const input = document.querySelector('[contenteditable=\\\"true\\\"]'); if(!input) return 'input not found'; return input.textContent.length === 0 ? 'sent ✓' : 'not sent: ' + input.textContent; }\"}\n```\n\n**发送成功后**：输入框被自动清空。\n\n---\n\n## 定位方法总结\n\n| 目标 | 定位策略 | 实测示例（参考）|\n|------|----------|----------------|\n| 「私信」按钮 | 文字=\"私信\"或\"私信数字\"，位于页面最右侧（`r.left > innerWidth - 200`）| 文字=\"私信15\"，viewport x:1708, y:28 |\n| 私信面板 | 宽 450-650px、高 500-750px、固定在页面右侧，内容含\"私信\" | class=`vgonMAXk`，581×649px，x:1235 |\n| 会话列表 | 面板左侧区域 | class=`zEpd_aAP`（宽~81px） |\n| 聊天详情 | 面板右侧区域 | class=`w5duGc5Q`（宽~500px） |\n| 输入框 | `contenteditable=\"true\"`，宽 200-500px，位于页面右侧 | class=`notranslate public-DraftEditor-content`，329×22px |\n| 发送按钮 | 输入框右侧；写入文本后点击 | class 示例：`PygT7Ced e2e-send-msg-btn` |\n\n> **注意**：以上尺寸和位置基于典型 1920px 宽屏幕实测。class name 会随抖音版本变化，**几何特征定位是更稳定的方法**。\n\n---\n\n## 读取具体聊天记录\n\n进入某个私聊或群聊后，可以读取聊天详情页中当前已加载的消息块。读取时建议按**消息块容器**处理，而不是直接读取整个聊天面板的合并文本。\n\n### 可识别字段\n\n| 字段 | 说明 |\n|------|------|\n| 时间 | 消息块上方或附近的时间文本，如「刚刚」「2026-04-04 19:07」 |\n| 消息内容 | 普通文本消息可从气泡文本中提取 |\n| 是否本人消息 | 可结合气泡位置、是否出现「撤回」操作等特征辅助判断 |\n| 群聊发送者 | 普通文本或引用消息中可能出现发送者名称；若 DOM 未明确暴露，应标注为未知/推断 |\n| 卡片类内容 | 视频、图集、点赞、撤回、不支持类型等可能只显示占位文本，不应过度解析 |\n\n### 示例：读取当前已加载消息块\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const area = document.querySelector('.IRB0Sra6') || document.querySelector('.z1iI1SFY'); if(!area) return 'message area not found'; const blocks = Array.from(area.querySelectorAll('.mM66nPpS')); return blocks.map(block => { const time = block.querySelector('.mA74174G')?.textContent?.trim() || ''; const text = (block.querySelector('.G3hOMUUp') || block.querySelector('.J3X6BOUb') || block).textContent.trim(); const mine = block.textContent.includes('撤回'); return { time, mine, text }; }); }\"}\n```\n\n> 上述 class name 是实测示例；正式逻辑应优先用消息区域的几何位置和消息块尺寸筛选，再结合文本特征解析。\n\n---\n\n## 获取对方回复\n\n### 滚动到最新消息\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const r = el.getBoundingClientRect(); if(r.width >= 400 && r.width <= 600 && r.height >= 300 && r.height <= 800 && r.left > 800) { el.scrollTop = el.scrollHeight; return 'scrolled'; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 1000}\n```\n\n### 截图确认\n\n```\nbrowser action=screenshot\n```\n\n---\n\n## 视频搜索与评论区操作（阶段性验证）\n\n当前已验证可完成以下只读/可定位操作：\n\n1. **按关键词搜索视频**：可直接打开 `https://www.douyin.com/search/<关键词>?type=video`，从搜索结果中提取 `/video/<id>` 链接、标题、作者、时间和互动数字。\n2. **打开指定视频/图文**：可直接导航到 `https://www.douyin.com/video/<id>` 或用户给出的分享链接。图文/笔记类内容可能会规范化跳转为 `/note/<id>`，个人页弹窗链接中的 `modal_id=<id>` 也可作为目标内容 ID 使用。\n3. **读取视频信息**：可从页面文本中读取标题、作者、发布时间、点赞/评论/收藏/分享等可见数字（具体字段需按页面布局解析）。\n4. **读取评论区**：视频页评论区在页面下方，需滚动到「全部评论」区域；可读取可见评论的昵称、内容、时间/地区、点赞数、分享/回复入口，并可继续向下滚动加载更多评论。\n5. **定位发评论输入框**：点击「留下你的精彩评论吧」后，会出现 Draft.js 输入框：`contenteditable=\"true\"` 且 class 包含 `public-DraftEditor-content`。\n6. **定位回复输入框**：点击某条评论的「回复」后，会在该评论下方出现 Draft.js 输入框，placeholder 形如「回复@用户名」。\n7. **定位发送按钮**：输入文字后，评论框右侧会出现图标按钮区，最右侧圆形上箭头按钮为发送/发布入口。\n8. **定位评论点赞入口**：每条评论的操作区包含点赞数、分享、回复；点赞图标/数字区域可通过评论块几何位置定位。\n9. **评论区情绪简报**：基于已加载评论文本，按正向/中性/负向/争议或信息不足分类，输出样本量、主要情绪、典型主题和置信度；样本少时必须注明限制。\n\n⚠️ 发评论、回复评论、点赞、分享等都属于外部互动写入操作，必须先获得用户明确确认；测试时可以定位输入框和按钮，但不要擅自提交。\n\n### 搜索并提取视频链接示例\n\n```javascript\nbrowser action=navigate profile=openclaw targetUrl=\"https://www.douyin.com/search/OpenClaw?type=video\"\nbrowser action=act request={\"kind\":\"wait\",\"timeMs\":3000}\nbrowser action=act request={\"kind\":\"evaluate\",\"fn\":\"() => Array.from(document.querySelectorAll('a[href*=\\\"/video/\\\"]')).map(a => ({ href: a.href, text: (a.innerText || a.textContent || '').trim() })).slice(0, 20)\"}\n```\n\n### 打开视频并滚动到评论区\n\n```javascript\nbrowser action=navigate profile=openclaw targetUrl=\"https://www.douyin.com/video/<video_id>\"\nbrowser action=act request={\"kind\":\"wait\",\"timeMs\":5000}\nbrowser action=act request={\"kind\":\"evaluate\",\"fn\":\"() => { const el = Array.from(document.querySelectorAll('*')).find(el => (el.innerText || el.textContent || '').includes('全部评论') && el.getBoundingClientRect().y > 100); if (el) { el.scrollIntoView({block:'start'}); return 'scrolled to comments'; } return 'comments not found'; }\"}\n```\n\n### 从个人页弹窗链接打开目标内容\n\n用户可能发送形如 `https://www.douyin.com/user/self?...&modal_id=<id>&showTab=like` 的链接。此类链接可直接打开；若页面进入个人页弹窗，也可提取 `modal_id` 后直接尝试：\n\n```javascript\nbrowser action=navigate profile=openclaw targetUrl=\"https://www.douyin.com/video/<modal_id>\"\nbrowser action=act request={\"kind\":\"wait\",\"timeMs\":5000}\n// 页面可能自动跳转为 /note/<modal_id>，属于正常情况。\n```\n\n### 读取可见评论示例\n\n```javascript\nbrowser action=act request={\"kind\":\"evaluate\",\"fn\":\"() => Array.from(document.querySelectorAll('*')).map(el => { const r = el.getBoundingClientRect(); const text = (el.innerText || el.textContent || '').trim(); return { r, text, cls: String(el.className || '') }; }).filter(o => o.r.width > 300 && o.r.height > 60 && o.text.includes('回复') && o.text.includes('·')).slice(0, 20).map(o => o.text)\"}\n```\n\n图文/笔记页的评论区可能在右侧栏，通过「评论(N)」标签切换；即使初始显示 `评论(0)`，点击后也可能加载真实评论。读取时以当前 DOM 文本为准，并检查是否出现「暂时没有更多评论」。\n\n### 评论区情绪简报模板\n\n```markdown\n评论区情绪简报：\n- 样本量：已加载 N 条评论\n- 整体情绪：正向 / 中性 / 负向 / 混合 / 信息不足\n- 主要主题：……\n- 风险/争议点：……\n- 置信度：高 / 中 / 低（说明原因）\n```\n\n### 评论/回复输入框定位\n\n```javascript\n// 点击「留下你的精彩评论吧」或某条评论的「回复」后：\nbrowser action=act request={\"kind\":\"evaluate\",\"fn\":\"() => { const input = document.querySelector('[contenteditable=\\\"true\\\"].public-DraftEditor-content'); if (!input) return 'input not found'; const r = input.getBoundingClientRect(); return { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height), text: input.innerText }; }\"}\n```\n\n---\n\n## 常见问题\n\n**Q: 点击「私信」按钮没反应？**\nA: 确认点击的是正确的按钮（顶栏最右侧，带\"私信\"文字）。也可先导航到 `/user/self` 页面再点击。\n\n**Q: 私信面板找不到？**\nA: 确认点击后有等待足够时间（`wait 2000ms`）。面板宽约 500px、高约 650px，固定在页面右侧。\n\n**Q: 输入框找不到？**\nA: 必须先在会话列表中点击一个具体用户，进入聊天详情模式，输入框才会出现。\n\n**Q: 消息输入了但发送不成功？**\nA: 先用 `kind: 'type'` 写入 Draft.js 输入框，再点击发送按钮。直接操作 DOM 文本（`textContent`赋值、`execCommand`、`clipboard paste`）无法触发 Draft.js 状态。\n\n**Q: 怎么确认发送成功了？**\nA: 发送成功后输入框被自动清空（textContent 变为空字符串）。\n\n---\n\n## 完整示例\n\n```javascript\n// 1. 打开抖音\nbrowser action=open profile=openclaw targetUrl=https://www.douyin.com/\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 3000}\n\n// 2. 点击「私信」按钮\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const t = (el.textContent||'').trim(); const r = el.getBoundingClientRect(); if((t === '私信' || t.startsWith('私信')) && r.width > 0 && r.height > 0 && r.width < 100 && r.left > window.innerWidth - 200) { el.click(); return 'clicked: ' + t; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n// 3. 点击目标用户\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const t = (el.textContent||'').trim(); const r = el.getBoundingClientRect(); if(t.includes('<目标用户名>') && r.width > 0 && r.height > 0) { el.click(); return 'clicked: ' + t; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n// 4. 写入消息并点击发送\nbrowser action=act request={\"kind\": \"type\", \"selector\": \"[contenteditable=\\\"true\\\"]\", \"text\": \"你好，这是测试消息\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 500}\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const btn = Array.from(document.querySelectorAll('*')).find(el => { const r = el.getBoundingClientRect(); const cls = String(el.className || ''); return r.width >= 20 && r.width <= 50 && r.height >= 20 && r.height <= 50 && r.left > window.innerWidth * 0.7 && r.top > window.innerHeight * 0.55 && (cls.includes('send') || cls.includes('PygT7Ced')); }); if(!btn) return 'send button not found'; btn.click(); return 'clicked send'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n// 5. 确认发送\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const input = document.querySelector('[contenteditable=\\\"true\\\"]'); return input ? (input.textContent.length === 0 ? 'sent ✓' : 'not sent: ' + input.textContent) : 'input not found'; }\"}\n```\n\nFile v1.1.9:_meta.json\n\n{\n  \"ownerId\": \"kn75k53tcdhqwka2fra5x817p9821zpb\",\n  \"slug\": \"douyin-messager\",\n  \"version\": \"1.1.9\",\n  \"publishedAt\": 1777914359131\n}\n\nArchive v1.1.8: 2 files, 7684 bytes\n\nFiles: _meta.json (134b), SKILL.md (21823b)\n\nFile v1.1.8:SKILL.md\n\n---\nname: Douyin Messager | 抖音私信助手\ndescription: \"Douyin DMs and video/note comment assistant. 抖音私信与视频/图文评论助手；可读私信、分析评论区，评论/回复等写入前必须确认。\"\n---\n\n# Douyin Messager | 抖音私信助手\n\n通过浏览器自动化发送抖音私信、获取聊天记录；也支持打开抖音视频/图文链接、读取评论区并做简要分析，在用户明确确认后执行评论/回复等写入操作。\n\n## 前置要求 | Prerequisites\n\n| 条件 | 说明 |\n|------|------|\n| **Browser profile** | 必须使用 `openclaw` profile |\n| **登录状态** | 抖音账号已在 `openclaw` profile 中保持登录态 |\n\n---\n\n## ⚠️ 执行前必须确认\n\n1. **用户已登录抖音账号**（可目测判断）\n2. **xdg-open 弹窗**：只在 Linux 下存在\n   - Linux：询问用户弹窗是否关闭\n   - Windows/macOS：跳过\n\n---\n\n## 当前能力边界（阶段性总结）\n\n本技能当前已验证可完成四类操作：\n\n1. **查看私信会话列表**：进入私信悬浮面板后，读取可见的私聊/群聊列表，区分会话名称、最新消息预览、时间、未读数和置顶状态。\n2. **进入具体聊天窗口**：从会话列表点击指定私聊或群聊，进入聊天详情页，读取当前已加载的聊天记录。\n3. **发送文本消息**：在聊天详情底部的 Draft.js 输入框中输入文本，并通过发送按钮完成发送。\n4. **视频/图文评论区处理**：可搜索或打开指定视频/图文链接，读取评论区，输出情绪简报；评论、回复、点赞等外部互动写入必须先获得用户明确确认。\n\n需要注意：网页版对视频、图集、点赞、撤回、暂不支持消息等卡片类内容的 DOM 暴露不完整，读取时应保守标注，不应强行推断完整内容或发送者。\n\n---\n\n## 核心思路\n\n> 抖音私信是一个**悬浮在页面右侧的动态面板**，包含\"会话列表\"和\"聊天详情\"两个视图。\n\n**会话列表布局**（类 QQ/微信）：左侧头像，中间分上下两行（上行：用户名/群名；下行：最新消息预览，群聊格式为「发消息的人：消息内容」），右侧时间戳。\n\n**聊天详情布局**：进入某个会话后，右侧显示聊天记录。普通文本消息通常可提取时间、消息内容，并可根据气泡方向、操作项（如是否有「撤回」）辅助判断是否为本人消息；群聊中他人文本消息有时可从消息文本或引用结构中识别发送者。卡片类消息需谨慎处理。\n\n**方法论**：不依赖固定 class name，而是通过元素的**几何特征 + 内容特征**动态查找。具体 class name 仅作参考示例。\n\n---\n\n## 获取私信列表流程\n\n获取私信的第一步不是进入某个聊天，而是**正确进入私信会话列表**。\n\n### 步骤 1：打开私信会话列表\n\n1. 打开抖音主页或个人主页。\n2. 在顶部导航栏右侧定位「私信」入口（可能显示未读数，如「私信11」）。\n3. 点击后等待右侧悬浮面板出现。\n4. 确认当前处于会话列表视图，而不是某个聊天详情页。\n\n### 步骤 2：识别每一行会话\n\n会话列表中每一行对应一个私聊或群聊。布局通常为：左侧头像；中间上行为会话名称（用户名/群名，可能带「置顶」标记）；中间下行为最新消息预览；右侧为时间戳或未读标记。\n\n⚠️ 会话列表本身是可滚动容器，不要只按当前可见区域判断数量。实测中滚动容器宽约 328px、高约 593px，`scrollHeight > clientHeight`，可通过 `scrollTop` 滚到底部；出现「暂时没有更多了」才表示到底。\n\n解析时应优先按**行容器**处理，而不是直接读取父元素的合并文本。读取每行时分别提取：\n\n| 字段 | 识别方法 |\n|------|----------|\n| 会话名称 | 行内上方文本，通常是用户名或群名；若有「置顶」应作为标记处理，不并入名称 |\n| 最新消息 | 行内下方文本；群聊常见格式为「发送者：消息内容」 |\n| 最新发送者 | 若最新消息包含「：」，冒号前通常是群聊中最新消息发送者 |\n| 时间 | 行右侧或下行末尾的时间文本，如「昨天」「04-25」「01:42」 |\n| 未读数 | 行右侧 badge 数字；顶部「私信（N）」通常表示总未读数 |\n| 置顶 | 名称行附近出现「置顶」标记时记录为 pinned=true |\n\n### 步骤 3：区分私聊与群聊\n\n- 私聊：上行通常是对方昵称；下行多为消息内容本身。\n- 群聊：上行是群名；下行常为「发送者：消息内容」。\n- 不要把上行群名和下行发送者合并为一个名称。\n\n**示例参考**：\n\n| 上行（会话名称） | 下行（最新消息） | 解析结果 |\n|------------------|------------------|----------|\n| `示例群A` | `成员甲：分享[视频] · 昨天` | 群名/会话名=`示例群A`，最新发送者=`成员甲` |\n| `项目交流群` | `成员乙：分享[图集] · 04-25` | 群名=`项目交流群`，最新发送者=`成员乙` |\n| `联系人A` + `置顶` | `收到，谢谢 · 01:42` | 私聊=`联系人A`，pinned=true |\n\n### 示例：提取可见会话行文本\n\n以下代码用于辅助观察当前可见列表，不应把 class name 当成稳定依赖：\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const rows = []; const all = Array.from(document.querySelectorAll('*')); for (const el of all) { const r = el.getBoundingClientRect(); const text = (el.textContent || '').trim(); if (r.width >= 250 && r.width <= 360 && r.height >= 50 && r.height <= 90 && r.left > window.innerWidth * 0.55 && text && !text.includes('私信（')) { rows.push({top: Math.round(r.top), text}); } } return rows.sort((a,b) => a.top - b.top); }\"}\n```\n\n---\n\n## 完整发送流程\n\n### 步骤 1：打开抖音主页\n\n```javascript\nbrowser action=open profile=openclaw targetUrl=https://www.douyin.com/\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 3000}\n```\n\n### 步骤 2：打开私信悬浮面板\n\n**找按钮**：在页面顶部导航栏中，找到文字为\"私信\"（可能带未读数如\"私信15\"）且位于页面最右侧的元素，点击它。\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const t = (el.textContent||'').trim(); const r = el.getBoundingClientRect(); if((t === '私信' || t.startsWith('私信')) && r.width > 0 && r.height > 0 && r.width < 100 && r.left > window.innerWidth - 200) { el.click(); return 'clicked: ' + t; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n```\n\n**找面板**：点击后，页面右侧应出现一个约 500-600px 宽、600-700px 高的悬浮面板。\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = document.querySelectorAll('[class]'); for(const el of all) { const r = el.getBoundingClientRect(); if(r.width >= 450 && r.width <= 650 && r.height >= 500 && r.height <= 750 && r.left > 0 && r.top > 0 && r.left < 1500) { const text = (el.textContent||'').trim(); if(text.includes('私信')) return 'found panel: ' + r.width + 'x' + r.height + ' at ' + Math.round(r.left); } } return 'panel not found'; }\"}\n```\n\n> **示例参考**：实测中面板容器 class 为 `vgonMAXk`，宽 581px、高 649px，固定在 x:1235 位置。但这是动态的，应以几何定位为主。\n\n### 步骤 3：进入具体聊天\n\n**找目标用户**：在面板的会话列表中，找到目标用户名的元素并点击。\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const t = (el.textContent||'').trim(); const r = el.getBoundingClientRect(); if(t.includes('<目标用户名>') && r.width > 0 && r.height > 0 && r.left > 0) { el.click(); return 'clicked: ' + t; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n```\n\n**成功标志**：面板从会话列表切换为聊天详情，底部出现输入区域。\n\n> **示例参考**：会话列表区域 class 为 `zEpd_aAP`（左侧，500px宽）；聊天详情区域 class 为 `w5duGc5Q`（右侧）。\n\n### 步骤 4：发送消息\n\n**找输入框**：聊天详情底部有一个 Draft.js 富文本编辑器（`contenteditable=\"true\"`，class 包含 `DraftEditor-content`）。\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = document.querySelectorAll('[contenteditable=\\\"true\\\"]'); for(const el of all) { const r = el.getBoundingClientRect(); if(r.width > 200 && r.width < 500 && r.height > 10 && r.height < 100 && r.left > 1000) return 'found input at ' + Math.round(r.left) + ',' + Math.round(r.top); } return 'input not found'; }\"}\n```\n\n**发送消息**：先用 `type` 方式写入 Draft.js 编辑器，再点击发送按钮。\n\n```javascript\n// 1. 写入文本\nbrowser action=act request={\"kind\": \"type\", \"selector\": \"[contenteditable=\\\"true\\\"]\", \"text\": \"你好，测试消息\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 500}\n\n// 2. 点击发送按钮（优先用几何位置查找，也可用实测 class 作为参考）\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const btn = Array.from(document.querySelectorAll('*')).find(el => { const r = el.getBoundingClientRect(); const cls = String(el.className || ''); return r.width >= 20 && r.width <= 50 && r.height >= 20 && r.height <= 50 && r.left > window.innerWidth * 0.7 && r.top > window.innerHeight * 0.55 && (cls.includes('send') || cls.includes('PygT7Ced')); }); if(!btn) return 'send button not found'; const r = btn.getBoundingClientRect(); btn.click(); return 'clicked send at ' + Math.round(r.left + r.width/2) + ',' + Math.round(r.top + r.height/2); }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n```\n\n> ⚠️ **必须用 `type` 写入 Draft.js 编辑器**。直接操作 DOM 文本（`textContent`、`execCommand`、`clipboard`）无法触发 Draft.js 内部状态，发送按钮会保持禁用。\n\n> **示例参考**：实测中输入框 class 为 `notranslate public-DraftEditor-content`（`contenteditable=\"true\"`），宽约 329px，位置 x:1346, y:651。\n\n### 步骤 5：确认发送成功\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const input = document.querySelector('[contenteditable=\\\"true\\\"]'); if(!input) return 'input not found'; return input.textContent.length === 0 ? 'sent ✓' : 'not sent: ' + input.textContent; }\"}\n```\n\n**发送成功后**：输入框被自动清空。\n\n---\n\n## 定位方法总结\n\n| 目标 | 定位策略 | 实测示例（参考）|\n|------|----------|----------------|\n| 「私信」按钮 | 文字=\"私信\"或\"私信数字\"，位于页面最右侧（`r.left > innerWidth - 200`）| 文字=\"私信15\"，viewport x:1708, y:28 |\n| 私信面板 | 宽 450-650px、高 500-750px、固定在页面右侧，内容含\"私信\" | class=`vgonMAXk`，581×649px，x:1235 |\n| 会话列表 | 面板左侧区域 | class=`zEpd_aAP`（宽~81px） |\n| 聊天详情 | 面板右侧区域 | class=`w5duGc5Q`（宽~500px） |\n| 输入框 | `contenteditable=\"true\"`，宽 200-500px，位于页面右侧 | class=`notranslate public-DraftEditor-content`，329×22px |\n| 发送按钮 | 输入框右侧；写入文本后点击 | class 示例：`PygT7Ced e2e-send-msg-btn` |\n\n> **注意**：以上尺寸和位置基于典型 1920px 宽屏幕实测。class name 会随抖音版本变化，**几何特征定位是更稳定的方法**。\n\n---\n\n## 读取具体聊天记录\n\n进入某个私聊或群聊后，可以读取聊天详情页中当前已加载的消息块。读取时建议按**消息块容器**处理，而不是直接读取整个聊天面板的合并文本。\n\n### 可识别字段\n\n| 字段 | 说明 |\n|------|------|\n| 时间 | 消息块上方或附近的时间文本，如「刚刚」「2026-04-04 19:07」 |\n| 消息内容 | 普通文本消息可从气泡文本中提取 |\n| 是否本人消息 | 可结合气泡位置、是否出现「撤回」操作等特征辅助判断 |\n| 群聊发送者 | 普通文本或引用消息中可能出现发送者名称；若 DOM 未明确暴露，应标注为未知/推断 |\n| 卡片类内容 | 视频、图集、点赞、撤回、不支持类型等可能只显示占位文本，不应过度解析 |\n\n### 示例：读取当前已加载消息块\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const area = document.querySelector('.IRB0Sra6') || document.querySelector('.z1iI1SFY'); if(!area) return 'message area not found'; const blocks = Array.from(area.querySelectorAll('.mM66nPpS')); return blocks.map(block => { const time = block.querySelector('.mA74174G')?.textContent?.trim() || ''; const text = (block.querySelector('.G3hOMUUp') || block.querySelector('.J3X6BOUb') || block).textContent.trim(); const mine = block.textContent.includes('撤回'); return { time, mine, text }; }); }\"}\n```\n\n> 上述 class name 是实测示例；正式逻辑应优先用消息区域的几何位置和消息块尺寸筛选，再结合文本特征解析。\n\n---\n\n## 获取对方回复\n\n### 滚动到最新消息\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const r = el.getBoundingClientRect(); if(r.width >= 400 && r.width <= 600 && r.height >= 300 && r.height <= 800 && r.left > 800) { el.scrollTop = el.scrollHeight; return 'scrolled'; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 1000}\n```\n\n### 截图确认\n\n```\nbrowser action=screenshot\n```\n\n---\n\n## 视频搜索与评论区操作（阶段性验证）\n\n当前已验证可完成以下只读/可定位操作：\n\n1. **按关键词搜索视频**：可直接打开 `https://www.douyin.com/search/<关键词>?type=video`，从搜索结果中提取 `/video/<id>` 链接、标题、作者、时间和互动数字。\n2. **打开指定视频/图文**：可直接导航到 `https://www.douyin.com/video/<id>` 或用户给出的分享链接。图文/笔记类内容可能会规范化跳转为 `/note/<id>`，个人页弹窗链接中的 `modal_id=<id>` 也可作为目标内容 ID 使用。\n3. **读取视频信息**：可从页面文本中读取标题、作者、发布时间、点赞/评论/收藏/分享等可见数字（具体字段需按页面布局解析）。\n4. **读取评论区**：视频页评论区在页面下方，需滚动到「全部评论」区域；可读取可见评论的昵称、内容、时间/地区、点赞数、分享/回复入口，并可继续向下滚动加载更多评论。\n5. **定位发评论输入框**：点击「留下你的精彩评论吧」后，会出现 Draft.js 输入框：`contenteditable=\"true\"` 且 class 包含 `public-DraftEditor-content`。\n6. **定位回复输入框**：点击某条评论的「回复」后，会在该评论下方出现 Draft.js 输入框，placeholder 形如「回复@用户名」。\n7. **定位发送按钮**：输入文字后，评论框右侧会出现图标按钮区，最右侧圆形上箭头按钮为发送/发布入口。\n8. **定位评论点赞入口**：每条评论的操作区包含点赞数、分享、回复；点赞图标/数字区域可通过评论块几何位置定位。\n9. **评论区情绪简报**：基于已加载评论文本，按正向/中性/负向/争议或信息不足分类，输出样本量、主要情绪、典型主题和置信度；样本少时必须注明限制。\n\n⚠️ 发评论、回复评论、点赞、分享等都属于外部互动写入操作，必须先获得用户明确确认；测试时可以定位输入框和按钮，但不要擅自提交。\n\n### 搜索并提取视频链接示例\n\n```javascript\nbrowser action=navigate profile=openclaw targetUrl=\"https://www.douyin.com/search/OpenClaw?type=video\"\nbrowser action=act request={\"kind\":\"wait\",\"timeMs\":3000}\nbrowser action=act request={\"kind\":\"evaluate\",\"fn\":\"() => Array.from(document.querySelectorAll('a[href*=\\\"/video/\\\"]')).map(a => ({ href: a.href, text: (a.innerText || a.textContent || '').trim() })).slice(0, 20)\"}\n```\n\n### 打开视频并滚动到评论区\n\n```javascript\nbrowser action=navigate profile=openclaw targetUrl=\"https://www.douyin.com/video/<video_id>\"\nbrowser action=act request={\"kind\":\"wait\",\"timeMs\":5000}\nbrowser action=act request={\"kind\":\"evaluate\",\"fn\":\"() => { const el = Array.from(document.querySelectorAll('*')).find(el => (el.innerText || el.textContent || '').includes('全部评论') && el.getBoundingClientRect().y > 100); if (el) { el.scrollIntoView({block:'start'}); return 'scrolled to comments'; } return 'comments not found'; }\"}\n```\n\n### 从个人页弹窗链接打开目标内容\n\n用户可能发送形如 `https://www.douyin.com/user/self?...&modal_id=<id>&showTab=like` 的链接。此类链接可直接打开；若页面进入个人页弹窗，也可提取 `modal_id` 后直接尝试：\n\n```javascript\nbrowser action=navigate profile=openclaw targetUrl=\"https://www.douyin.com/video/<modal_id>\"\nbrowser action=act request={\"kind\":\"wait\",\"timeMs\":5000}\n// 页面可能自动跳转为 /note/<modal_id>，属于正常情况。\n```\n\n### 读取可见评论示例\n\n```javascript\nbrowser action=act request={\"kind\":\"evaluate\",\"fn\":\"() => Array.from(document.querySelectorAll('*')).map(el => { const r = el.getBoundingClientRect(); const text = (el.innerText || el.textContent || '').trim(); return { r, text, cls: String(el.className || '') }; }).filter(o => o.r.width > 300 && o.r.height > 60 && o.text.includes('回复') && o.text.includes('·')).slice(0, 20).map(o => o.text)\"}\n```\n\n图文/笔记页的评论区可能在右侧栏，通过「评论(N)」标签切换；即使初始显示 `评论(0)`，点击后也可能加载真实评论。读取时以当前 DOM 文本为准，并检查是否出现「暂时没有更多评论」。\n\n### 评论区情绪简报模板\n\n```markdown\n评论区情绪简报：\n- 样本量：已加载 N 条评论\n- 整体情绪：正向 / 中性 / 负向 / 混合 / 信息不足\n- 主要主题：……\n- 风险/争议点：……\n- 置信度：高 / 中 / 低（说明原因）\n```\n\n### 评论/回复输入框定位\n\n```javascript\n// 点击「留下你的精彩评论吧」或某条评论的「回复」后：\nbrowser action=act request={\"kind\":\"evaluate\",\"fn\":\"() => { const input = document.querySelector('[contenteditable=\\\"true\\\"].public-DraftEditor-content'); if (!input) return 'input not found'; const r = input.getBoundingClientRect(); return { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height), text: input.innerText }; }\"}\n```\n\n---\n\n## 常见问题\n\n**Q: 点击「私信」按钮没反应？**\nA: 确认点击的是正确的按钮（顶栏最右侧，带\"私信\"文字）。也可先导航到 `/user/self` 页面再点击。\n\n**Q: 私信面板找不到？**\nA: 确认点击后有等待足够时间（`wait 2000ms`）。面板宽约 500px、高约 650px，固定在页面右侧。\n\n**Q: 输入框找不到？**\nA: 必须先在会话列表中点击一个具体用户，进入聊天详情模式，输入框才会出现。\n\n**Q: 消息输入了但发送不成功？**\nA: 先用 `kind: 'type'` 写入 Draft.js 输入框，再点击发送按钮。直接操作 DOM 文本（`textContent`赋值、`execCommand`、`clipboard paste`）无法触发 Draft.js 状态。\n\n**Q: 怎么确认发送成功了？**\nA: 发送成功后输入框被自动清空（textContent 变为空字符串）。\n\n---\n\n## 完整示例\n\n```javascript\n// 1. 打开抖音\nbrowser action=open profile=openclaw targetUrl=https://www.douyin.com/\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 3000}\n\n// 2. 点击「私信」按钮\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const t = (el.textContent||'').trim(); const r = el.getBoundingClientRect(); if((t === '私信' || t.startsWith('私信')) && r.width > 0 && r.height > 0 && r.width < 100 && r.left > window.innerWidth - 200) { el.click(); return 'clicked: ' + t; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n// 3. 点击目标用户\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const t = (el.textContent||'').trim(); const r = el.getBoundingClientRect(); if(t.includes('<目标用户名>') && r.width > 0 && r.height > 0) { el.click(); return 'clicked: ' + t; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n// 4. 写入消息并点击发送\nbrowser action=act request={\"kind\": \"type\", \"selector\": \"[contenteditable=\\\"true\\\"]\", \"text\": \"你好，这是测试消息\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 500}\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const btn = Array.from(document.querySelectorAll('*')).find(el => { const r = el.getBoundingClientRect(); const cls = String(el.className || ''); return r.width >= 20 && r.width <= 50 && r.height >= 20 && r.height <= 50 && r.left > window.innerWidth * 0.7 && r.top > window.innerHeight * 0.55 && (cls.includes('send') || cls.includes('PygT7Ced')); }); if(!btn) return 'send button not found'; btn.click(); return 'clicked send'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n// 5. 确认发送\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const input = document.querySelector('[contenteditable=\\\"true\\\"]'); return input ? (input.textContent.length === 0 ? 'sent ✓' : 'not sent: ' + input.textContent) : 'input not found'; }\"}\n```\n\nFile v1.1.8:_meta.json\n\n{\n  \"ownerId\": \"kn75k53tcdhqwka2fra5x817p9821zpb\",\n  \"slug\": \"douyin-messager\",\n  \"version\": \"1.1.8\",\n  \"publishedAt\": 1777513836464\n}\n\nArchive v1.1.7: 2 files, 7771 bytes\n\nFiles: _meta.json (134b), SKILL.md (21965b)\n\nFile v1.1.7:SKILL.md\n\n---\nname: Douyin Messager | 抖音私信助手\ndescription: \"Send Douyin DMs, read chat history, search/open videos or notes, read/analyze comments, and comment/reply after explicit confirmation through browser automation. 自动发送抖音私信、读取聊天记录、搜索/打开视频或图文、读取/分析评论区，并在明确确认后评论/回复。\"\n---\n\n# Douyin Messager | 抖音私信助手\n\n通过浏览器自动化发送抖音私信、获取聊天记录；也支持打开抖音视频/图文链接、读取评论区并做简要分析，在用户明确确认后执行评论/回复等写入操作。\n\n## 前置要求 | Prerequisites\n\n| 条件 | 说明 |\n|------|------|\n| **Browser profile** | 必须使用 `openclaw` profile |\n| **登录状态** | 抖音账号已在 `openclaw` profile 中保持登录态 |\n\n---\n\n## ⚠️ 执行前必须确认\n\n1. **用户已登录抖音账号**（可目测判断）\n2. **xdg-open 弹窗**：只在 Linux 下存在\n   - Linux：询问用户弹窗是否关闭\n   - Windows/macOS：跳过\n\n---\n\n## 当前能力边界（阶段性总结）\n\n本技能当前已验证可完成四类操作：\n\n1. **查看私信会话列表**：进入私信悬浮面板后，读取可见的私聊/群聊列表，区分会话名称、最新消息预览、时间、未读数和置顶状态。\n2. **进入具体聊天窗口**：从会话列表点击指定私聊或群聊，进入聊天详情页，读取当前已加载的聊天记录。\n3. **发送文本消息**：在聊天详情底部的 Draft.js 输入框中输入文本，并通过发送按钮完成发送。\n4. **视频/图文评论区处理**：可搜索或打开指定视频/图文链接，读取评论区，输出情绪简报；评论、回复、点赞等外部互动写入必须先获得用户明确确认。\n\n需要注意：网页版对视频、图集、点赞、撤回、暂不支持消息等卡片类内容的 DOM 暴露不完整，读取时应保守标注，不应强行推断完整内容或发送者。\n\n---\n\n## 核心思路\n\n> 抖音私信是一个**悬浮在页面右侧的动态面板**，包含\"会话列表\"和\"聊天详情\"两个视图。\n\n**会话列表布局**（类 QQ/微信）：左侧头像，中间分上下两行（上行：用户名/群名；下行：最新消息预览，群聊格式为「发消息的人：消息内容」），右侧时间戳。\n\n**聊天详情布局**：进入某个会话后，右侧显示聊天记录。普通文本消息通常可提取时间、消息内容，并可根据气泡方向、操作项（如是否有「撤回」）辅助判断是否为本人消息；群聊中他人文本消息有时可从消息文本或引用结构中识别发送者。卡片类消息需谨慎处理。\n\n**方法论**：不依赖固定 class name，而是通过元素的**几何特征 + 内容特征**动态查找。具体 class name 仅作参考示例。\n\n---\n\n## 获取私信列表流程\n\n获取私信的第一步不是进入某个聊天，而是**正确进入私信会话列表**。\n\n### 步骤 1：打开私信会话列表\n\n1. 打开抖音主页或个人主页。\n2. 在顶部导航栏右侧定位「私信」入口（可能显示未读数，如「私信11」）。\n3. 点击后等待右侧悬浮面板出现。\n4. 确认当前处于会话列表视图，而不是某个聊天详情页。\n\n### 步骤 2：识别每一行会话\n\n会话列表中每一行对应一个私聊或群聊。布局通常为：左侧头像；中间上行为会话名称（用户名/群名，可能带「置顶」标记）；中间下行为最新消息预览；右侧为时间戳或未读标记。\n\n⚠️ 会话列表本身是可滚动容器，不要只按当前可见区域判断数量。实测中滚动容器宽约 328px、高约 593px，`scrollHeight > clientHeight`，可通过 `scrollTop` 滚到底部；出现「暂时没有更多了」才表示到底。\n\n解析时应优先按**行容器**处理，而不是直接读取父元素的合并文本。读取每行时分别提取：\n\n| 字段 | 识别方法 |\n|------|----------|\n| 会话名称 | 行内上方文本，通常是用户名或群名；若有「置顶」应作为标记处理，不并入名称 |\n| 最新消息 | 行内下方文本；群聊常见格式为「发送者：消息内容」 |\n| 最新发送者 | 若最新消息包含「：」，冒号前通常是群聊中最新消息发送者 |\n| 时间 | 行右侧或下行末尾的时间文本，如「昨天」「04-25」「01:42」 |\n| 未读数 | 行右侧 badge 数字；顶部「私信（N）」通常表示总未读数 |\n| 置顶 | 名称行附近出现「置顶」标记时记录为 pinned=true |\n\n### 步骤 3：区分私聊与群聊\n\n- 私聊：上行通常是对方昵称；下行多为消息内容本身。\n- 群聊：上行是群名；下行常为「发送者：消息内容」。\n- 不要把上行群名和下行发送者合并为一个名称。\n\n**示例参考**：\n\n| 上行（会话名称） | 下行（最新消息） | 解析结果 |\n|------------------|------------------|----------|\n| `示例群A` | `成员甲：分享[视频] · 昨天` | 群名/会话名=`示例群A`，最新发送者=`成员甲` |\n| `项目交流群` | `成员乙：分享[图集] · 04-25` | 群名=`项目交流群`，最新发送者=`成员乙` |\n| `联系人A` + `置顶` | `收到，谢谢 · 01:42` | 私聊=`联系人A`，pinned=true |\n\n### 示例：提取可见会话行文本\n\n以下代码用于辅助观察当前可见列表，不应把 class name 当成稳定依赖：\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const rows = []; const all = Array.from(document.querySelectorAll('*')); for (const el of all) { const r = el.getBoundingClientRect(); const text = (el.textContent || '').trim(); if (r.width >= 250 && r.width <= 360 && r.height >= 50 && r.height <= 90 && r.left > window.innerWidth * 0.55 && text && !text.includes('私信（')) { rows.push({top: Math.round(r.top), text}); } } return rows.sort((a,b) => a.top - b.top); }\"}\n```\n\n---\n\n## 完整发送流程\n\n### 步骤 1：打开抖音主页\n\n```javascript\nbrowser action=open profile=openclaw targetUrl=https://www.douyin.com/\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 3000}\n```\n\n### 步骤 2：打开私信悬浮面板\n\n**找按钮**：在页面顶部导航栏中，找到文字为\"私信\"（可能带未读数如\"私信15\"）且位于页面最右侧的元素，点击它。\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const t = (el.textContent||'').trim(); const r = el.getBoundingClientRect(); if((t === '私信' || t.startsWith('私信')) && r.width > 0 && r.height > 0 && r.width < 100 && r.left > window.innerWidth - 200) { el.click(); return 'clicked: ' + t; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n```\n\n**找面板**：点击后，页面右侧应出现一个约 500-600px 宽、600-700px 高的悬浮面板。\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = document.querySelectorAll('[class]'); for(const el of all) { const r = el.getBoundingClientRect(); if(r.width >= 450 && r.width <= 650 && r.height >= 500 && r.height <= 750 && r.left > 0 && r.top > 0 && r.left < 1500) { const text = (el.textContent||'').trim(); if(text.includes('私信')) return 'found panel: ' + r.width + 'x' + r.height + ' at ' + Math.round(r.left); } } return 'panel not found'; }\"}\n```\n\n> **示例参考**：实测中面板容器 class 为 `vgonMAXk`，宽 581px、高 649px，固定在 x:1235 位置。但这是动态的，应以几何定位为主。\n\n### 步骤 3：进入具体聊天\n\n**找目标用户**：在面板的会话列表中，找到目标用户名的元素并点击。\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const t = (el.textContent||'').trim(); const r = el.getBoundingClientRect(); if(t.includes('<目标用户名>') && r.width > 0 && r.height > 0 && r.left > 0) { el.click(); return 'clicked: ' + t; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n```\n\n**成功标志**：面板从会话列表切换为聊天详情，底部出现输入区域。\n\n> **示例参考**：会话列表区域 class 为 `zEpd_aAP`（左侧，500px宽）；聊天详情区域 class 为 `w5duGc5Q`（右侧）。\n\n### 步骤 4：发送消息\n\n**找输入框**：聊天详情底部有一个 Draft.js 富文本编辑器（`contenteditable=\"true\"`，class 包含 `DraftEditor-content`）。\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = document.querySelectorAll('[contenteditable=\\\"true\\\"]'); for(const el of all) { const r = el.getBoundingClientRect(); if(r.width > 200 && r.width < 500 && r.height > 10 && r.height < 100 && r.left > 1000) return 'found input at ' + Math.round(r.left) + ',' + Math.round(r.top); } return 'input not found'; }\"}\n```\n\n**发送消息**：先用 `type` 方式写入 Draft.js 编辑器，再点击发送按钮。\n\n```javascript\n// 1. 写入文本\nbrowser action=act request={\"kind\": \"type\", \"selector\": \"[contenteditable=\\\"true\\\"]\", \"text\": \"你好，测试消息\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 500}\n\n// 2. 点击发送按钮（优先用几何位置查找，也可用实测 class 作为参考）\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const btn = Array.from(document.querySelectorAll('*')).find(el => { const r = el.getBoundingClientRect(); const cls = String(el.className || ''); return r.width >= 20 && r.width <= 50 && r.height >= 20 && r.height <= 50 && r.left > window.innerWidth * 0.7 && r.top > window.innerHeight * 0.55 && (cls.includes('send') || cls.includes('PygT7Ced')); }); if(!btn) return 'send button not found'; const r = btn.getBoundingClientRect(); btn.click(); return 'clicked send at ' + Math.round(r.left + r.width/2) + ',' + Math.round(r.top + r.height/2); }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n```\n\n> ⚠️ **必须用 `type` 写入 Draft.js 编辑器**。直接操作 DOM 文本（`textContent`、`execCommand`、`clipboard`）无法触发 Draft.js 内部状态，发送按钮会保持禁用。\n\n> **示例参考**：实测中输入框 class 为 `notranslate public-DraftEditor-content`（`contenteditable=\"true\"`），宽约 329px，位置 x:1346, y:651。\n\n### 步骤 5：确认发送成功\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const input = document.querySelector('[contenteditable=\\\"true\\\"]'); if(!input) return 'input not found'; return input.textContent.length === 0 ? 'sent ✓' : 'not sent: ' + input.textContent; }\"}\n```\n\n**发送成功后**：输入框被自动清空。\n\n---\n\n## 定位方法总结\n\n| 目标 | 定位策略 | 实测示例（参考）|\n|------|----------|----------------|\n| 「私信」按钮 | 文字=\"私信\"或\"私信数字\"，位于页面最右侧（`r.left > innerWidth - 200`）| 文字=\"私信15\"，viewport x:1708, y:28 |\n| 私信面板 | 宽 450-650px、高 500-750px、固定在页面右侧，内容含\"私信\" | class=`vgonMAXk`，581×649px，x:1235 |\n| 会话列表 | 面板左侧区域 | class=`zEpd_aAP`（宽~81px） |\n| 聊天详情 | 面板右侧区域 | class=`w5duGc5Q`（宽~500px） |\n| 输入框 | `contenteditable=\"true\"`，宽 200-500px，位于页面右侧 | class=`notranslate public-DraftEditor-content`，329×22px |\n| 发送按钮 | 输入框右侧；写入文本后点击 | class 示例：`PygT7Ced e2e-send-msg-btn` |\n\n> **注意**：以上尺寸和位置基于典型 1920px 宽屏幕实测。class name 会随抖音版本变化，**几何特征定位是更稳定的方法**。\n\n---\n\n## 读取具体聊天记录\n\n进入某个私聊或群聊后，可以读取聊天详情页中当前已加载的消息块。读取时建议按**消息块容器**处理，而不是直接读取整个聊天面板的合并文本。\n\n### 可识别字段\n\n| 字段 | 说明 |\n|------|------|\n| 时间 | 消息块上方或附近的时间文本，如「刚刚」「2026-04-04 19:07」 |\n| 消息内容 | 普通文本消息可从气泡文本中提取 |\n| 是否本人消息 | 可结合气泡位置、是否出现「撤回」操作等特征辅助判断 |\n| 群聊发送者 | 普通文本或引用消息中可能出现发送者名称；若 DOM 未明确暴露，应标注为未知/推断 |\n| 卡片类内容 | 视频、图集、点赞、撤回、不支持类型等可能只显示占位文本，不应过度解析 |\n\n### 示例：读取当前已加载消息块\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const area = document.querySelector('.IRB0Sra6') || document.querySelector('.z1iI1SFY'); if(!area) return 'message area not found'; const blocks = Array.from(area.querySelectorAll('.mM66nPpS')); return blocks.map(block => { const time = block.querySelector('.mA74174G')?.textContent?.trim() || ''; const text = (block.querySelector('.G3hOMUUp') || block.querySelector('.J3X6BOUb') || block).textContent.trim(); const mine = block.textContent.includes('撤回'); return { time, mine, text }; }); }\"}\n```\n\n> 上述 class name 是实测示例；正式逻辑应优先用消息区域的几何位置和消息块尺寸筛选，再结合文本特征解析。\n\n---\n\n## 获取对方回复\n\n### 滚动到最新消息\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const r = el.getBoundingClientRect(); if(r.width >= 400 && r.width <= 600 && r.height >= 300 && r.height <= 800 && r.left > 800) { el.scrollTop = el.scrollHeight; return 'scrolled'; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 1000}\n```\n\n### 截图确认\n\n```\nbrowser action=screenshot\n```\n\n---\n\n## 视频搜索与评论区操作（阶段性验证）\n\n当前已验证可完成以下只读/可定位操作：\n\n1. **按关键词搜索视频**：可直接打开 `https://www.douyin.com/search/<关键词>?type=video`，从搜索结果中提取 `/video/<id>` 链接、标题、作者、时间和互动数字。\n2. **打开指定视频/图文**：可直接导航到 `https://www.douyin.com/video/<id>` 或用户给出的分享链接。图文/笔记类内容可能会规范化跳转为 `/note/<id>`，个人页弹窗链接中的 `modal_id=<id>` 也可作为目标内容 ID 使用。\n3. **读取视频信息**：可从页面文本中读取标题、作者、发布时间、点赞/评论/收藏/分享等可见数字（具体字段需按页面布局解析）。\n4. **读取评论区**：视频页评论区在页面下方，需滚动到「全部评论」区域；可读取可见评论的昵称、内容、时间/地区、点赞数、分享/回复入口，并可继续向下滚动加载更多评论。\n5. **定位发评论输入框**：点击「留下你的精彩评论吧」后，会出现 Draft.js 输入框：`contenteditable=\"true\"` 且 class 包含 `public-DraftEditor-content`。\n6. **定位回复输入框**：点击某条评论的「回复」后，会在该评论下方出现 Draft.js 输入框，placeholder 形如「回复@用户名」。\n7. **定位发送按钮**：输入文字后，评论框右侧会出现图标按钮区，最右侧圆形上箭头按钮为发送/发布入口。\n8. **定位评论点赞入口**：每条评论的操作区包含点赞数、分享、回复；点赞图标/数字区域可通过评论块几何位置定位。\n9. **评论区情绪简报**：基于已加载评论文本，按正向/中性/负向/争议或信息不足分类，输出样本量、主要情绪、典型主题和置信度；样本少时必须注明限制。\n\n⚠️ 发评论、回复评论、点赞、分享等都属于外部互动写入操作，必须先获得用户明确确认；测试时可以定位输入框和按钮，但不要擅自提交。\n\n### 搜索并提取视频链接示例\n\n```javascript\nbrowser action=navigate profile=openclaw targetUrl=\"https://www.douyin.com/search/OpenClaw?type=video\"\nbrowser action=act request={\"kind\":\"wait\",\"timeMs\":3000}\nbrowser action=act request={\"kind\":\"evaluate\",\"fn\":\"() => Array.from(document.querySelectorAll('a[href*=\\\"/video/\\\"]')).map(a => ({ href: a.href, text: (a.innerText || a.textContent || '').trim() })).slice(0, 20)\"}\n```\n\n### 打开视频并滚动到评论区\n\n```javascript\nbrowser action=navigate profile=openclaw targetUrl=\"https://www.douyin.com/video/<video_id>\"\nbrowser action=act request={\"kind\":\"wait\",\"timeMs\":5000}\nbrowser action=act request={\"kind\":\"evaluate\",\"fn\":\"() => { const el = Array.from(document.querySelectorAll('*')).find(el => (el.innerText || el.textContent || '').includes('全部评论') && el.getBoundingClientRect().y > 100); if (el) { el.scrollIntoView({block:'start'}); return 'scrolled to comments'; } return 'comments not found'; }\"}\n```\n\n### 从个人页弹窗链接打开目标内容\n\n用户可能发送形如 `https://www.douyin.com/user/self?...&modal_id=<id>&showTab=like` 的链接。此类链接可直接打开；若页面进入个人页弹窗，也可提取 `modal_id` 后直接尝试：\n\n```javascript\nbrowser action=navigate profile=openclaw targetUrl=\"https://www.douyin.com/video/<modal_id>\"\nbrowser action=act request={\"kind\":\"wait\",\"timeMs\":5000}\n// 页面可能自动跳转为 /note/<modal_id>，属于正常情况。\n```\n\n### 读取可见评论示例\n\n```javascript\nbrowser action=act request={\"kind\":\"evaluate\",\"fn\":\"() => Array.from(document.querySelectorAll('*')).map(el => { const r = el.getBoundingClientRect(); const text = (el.innerText || el.textContent || '').trim(); return { r, text, cls: String(el.className || '') }; }).filter(o => o.r.width > 300 && o.r.height > 60 && o.text.includes('回复') && o.text.includes('·')).slice(0, 20).map(o => o.text)\"}\n```\n\n图文/笔记页的评论区可能在右侧栏，通过「评论(N)」标签切换；即使初始显示 `评论(0)`，点击后也可能加载真实评论。读取时以当前 DOM 文本为准，并检查是否出现「暂时没有更多评论」。\n\n### 评论区情绪简报模板\n\n```markdown\n评论区情绪简报：\n- 样本量：已加载 N 条评论\n- 整体情绪：正向 / 中性 / 负向 / 混合 / 信息不足\n- 主要主题：……\n- 风险/争议点：……\n- 置信度：高 / 中 / 低（说明原因）\n```\n\n### 评论/回复输入框定位\n\n```javascript\n// 点击「留下你的精彩评论吧」或某条评论的「回复」后：\nbrowser action=act request={\"kind\":\"evaluate\",\"fn\":\"() => { const input = document.querySelector('[contenteditable=\\\"true\\\"].public-DraftEditor-content'); if (!input) return 'input not found'; const r = input.getBoundingClientRect(); return { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height), text: input.innerText }; }\"}\n```\n\n---\n\n## 常见问题\n\n**Q: 点击「私信」按钮没反应？**\nA: 确认点击的是正确的按钮（顶栏最右侧，带\"私信\"文字）。也可先导航到 `/user/self` 页面再点击。\n\n**Q: 私信面板找不到？**\nA: 确认点击后有等待足够时间（`wait 2000ms`）。面板宽约 500px、高约 650px，固定在页面右侧。\n\n**Q: 输入框找不到？**\nA: 必须先在会话列表中点击一个具体用户，进入聊天详情模式，输入框才会出现。\n\n**Q: 消息输入了但发送不成功？**\nA: 先用 `kind: 'type'` 写入 Draft.js 输入框，再点击发送按钮。直接操作 DOM 文本（`textContent`赋值、`execCommand`、`clipboard paste`）无法触发 Draft.js 状态。\n\n**Q: 怎么确认发送成功了？**\nA: 发送成功后输入框被自动清空（textContent 变为空字符串）。\n\n---\n\n## 完整示例\n\n```javascript\n// 1. 打开抖音\nbrowser action=open profile=openclaw targetUrl=https://www.douyin.com/\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 3000}\n\n// 2. 点击「私信」按钮\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const t = (el.textContent||'').trim(); const r = el.getBoundingClientRect(); if((t === '私信' || t.startsWith('私信')) && r.width > 0 && r.height > 0 && r.width < 100 && r.left > window.innerWidth - 200) { el.click(); return 'clicked: ' + t; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n// 3. 点击目标用户\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const t = (el.textContent||'').trim(); const r = el.getBoundingClientRect(); if(t.includes('<目标用户名>') && r.width > 0 && r.height > 0) { el.click(); return 'clicked: ' + t; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n// 4. 写入消息并点击发送\nbrowser action=act request={\"kind\": \"type\", \"selector\": \"[contenteditable=\\\"true\\\"]\", \"text\": \"你好，这是测试消息\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 500}\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const btn = Array.from(document.querySelectorAll('*')).find(el => { const r = el.getBoundingClientRect(); const cls = String(el.className || ''); return r.width >= 20 && r.width <= 50 && r.height >= 20 && r.height <= 50 && r.left > window.innerWidth * 0.7 && r.top > window.innerHeight * 0.55 && (cls.includes('send') || cls.includes('PygT7Ced')); }); if(!btn) return 'send button not found'; btn.click(); return 'clicked send'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n// 5. 确认发送\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const input = document.querySelector('[contenteditable=\\\"true\\\"]'); return input ? (input.textContent.length === 0 ? 'sent ✓' : 'not sent: ' + input.textContent) : 'input not found'; }\"}\n```\n\nFile v1.1.7:_meta.json\n\n{\n  \"ownerId\": \"kn75k53tcdhqwka2fra5x817p9821zpb\",\n  \"slug\": \"douyin-messager\",\n  \"version\": \"1.1.7\",\n  \"publishedAt\": 1777513573606\n}\n\nArchive v1.1.6: 2 files, 3335 bytes\n\nFiles: _meta.json (134b), SKILL.md (9317b)\n\nFile v1.1.6:SKILL.md\n\n---\nname: Douyin Messager | 抖音私信助手\ndescription: \"Send Douyin DMs, reply, and check chat history through browser automation. 自动发送抖音私信、回复消息、查看聊天记录。\"\n---\n\n# Douyin Messager | 抖音私信助手\n\n通过浏览器自动化发送抖音私信、获取聊天记录。\n\n## 前置要求 | Prerequisites\n\n> 使用本技能前请先确认以下条件。\n\n| 条件 | 说明 |\n|------|------|\n| **Browser profile** | 必须使用 `openclaw` profile（已通过 `browser` 工具启动并登录抖音） |\n| **登录状态** | 抖音账号已在 `openclaw` profile 中保持登录态 |\n\n### 为什么需要 openclaw profile？\n\n本技能通过在抖音页面执行 DOM JavaScript（`querySelector`、`clipboard`、键盘事件等）实现消息发送。这些操作需要浏览器环境持有有效的抖音登录会话。\n\n- `openclaw` profile 由 OpenClaw 启动和管理，独立于 Morois 的日常浏览器 profile\n- 技能仅在抖音域名（`douyin.com`）内执行 DOM 操作，无法访问其他网站\n- 任何对外发送操作均需用户明确确认，不会自动发送\n\n---\n\n## ⚠️ 执行前必须确认\n\n1. **用户已登录抖音账号**（通常已登录，可目测判断）\n\n**xdg-open 弹窗：只在 Linux 下存在，Windows/macOS 不需要检查。**\n- Windows：`shell=powershell` 或 `os=Windows_NT`，**跳过 xdg-open 检查**\n- Linux：`shell=bash`，**询问用户是否关闭了弹窗**\n\n---\n\n## 发送私信流程\n\n### 步骤 1：打开抖音\n\n1. 打开抖音主页 → `browser action=open profile=openclaw targetUrl=https://www.douyin.com/`\n2. **xdg-open 弹窗在这步出现**（因为打开了抖音主页）\n   - Linux：`shell=bash`，**询问用户弹窗是否关闭**\n     - 弹窗未关闭 → 等待用户关闭 → 确认后再继续\n     - 弹窗已关闭或无弹窗 → 继续步骤 2\n   - Windows/macOS：跳过弹窗检查\n3. 继续步骤 2\n\n### 步骤 2：打开私信面板\n\n**方式 A：置顶联系人（最快）**\n1. 获取快照找到私信按钮，点击打开私信面板\n2. 在私信列表中找目标用户名（注意：列表只显示最近联系人，不滚动看不到中间部分）\n3. 点击目标 → 打开聊天窗口\n\n**方式 B：搜索用户（普适）**\n1. 点击顶栏搜索框，输入用户名\n2. 在搜索结果中找到目标（优先选有\"相互关注\"标识的），点击进入主页\n3. 点击主页的\"私信\"按钮 → 私信面板自动打开\n\n> **注意**：私信面板是动态浮层，每次快照后元素 ref 会变化。优先用 JS evaluate 定位元素，少用 ref 点击。\n\n### 步骤 3：用 JS 操作 Draft.js 输入框\n\n**私信输入框是 Draft.js 富文本编辑器（`contenteditable`），不是普通 input，不能用 `type` 指令直接操作。**\n\n**第一步：找到编辑器**\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = document.querySelectorAll('*'); let panel = null; for (const el of all) { const r = el.getBoundingClientRect(); if (r.width >= 300 && r.width <= 400 && r.height >= 400 && r.height <= 700 && r.left >= 870 && r.left <= 1010) { if (el.textContent.includes('<目标用户名>') && el.textContent.includes('发送消息')) { panel = el; break; } } } if (!panel) return 'panel not found'; const editor = panel.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; return 'found'; }\"}\n```\n\n如果返回 `panel not found`，说明私信面板未打开，重新执行步骤 2。\n\n**第二步：用剪贴板粘贴（绕过 Draft.js 事件拦截）**\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; (editor.parentElement || editor).focus(); navigator.clipboard.writeText('消息内容').then(() => document.execCommand('paste')); return 'ok'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 300}\n```\n\n**第三步：确认已写入**\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; return editor.textContent.includes('消息内容') ? 'ok' : 'not written'; }\"}\n```\n\n**第四步：发送（Enter 键）**\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; editor.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true})); return 'sent'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 1000}\n```\n\n### 步骤 4：确认发送成功\n\n获取快照，检查私信列表中是否出现 `消息内容 · 刚刚`。\n\n---\n\n## 获取对方回复\n\n### 滚动到底部\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const panel = document.querySelector('[class*=\\\"w5duGc5Q\\\"], [class*=\\\"RoMuFUzT\\\"]'); if (!panel) return 'panel not found'; panel.scrollTop = panel.scrollHeight; return 'scrolled'; }\"}\n```\n\n### 截图确认\n\n```\nbrowser action=screenshot\n```\n\n新消息气泡在可视区域底部之外，先滚动再截图。\n\n---\n\n## 完整示例（方式 A：置顶联系人）\n\n```\n# 1. 打开抖音\nbrowser action=open profile=openclaw targetUrl=https://www.douyin.com/\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n# 2. 打开私信面板（JS 点击私信按钮）\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = document.querySelectorAll('*'); for (const el of all) { if ((el.textContent || '').trim() === '私信' && el.getBoundingClientRect().width < 100) { el.click(); return 'clicked'; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 800}\n\n# 3. 在私信列表中点击目标\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = document.querySelectorAll('[class*=\\\"w5duGc5Q\\\"], [class*=\\\"RoMuFUzT\\\"]'); for (const p of all) { const r = p.getBoundingClientRect(); if (r.width === 500 && Math.abs(r.left - 1006) < 50) { const items = p.querySelectorAll('[class*=\\\"cursor-pointer\\\"]'); for (const item of items) { if ((item.textContent || '').includes('<目标用户名>')) { item.click(); return 'clicked target'; } } } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 800}\n\n# 4. 粘贴消息\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; (editor.parentElement || editor).focus(); navigator.clipboard.writeText('你好').then(() => document.execCommand('paste')); return 'ok'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 300}\n\n# 5. 发送\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); editor.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true})); return 'sent'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 1000}\n\n# 6. 确认\nbrowser action=snapshot\n```\n\n---\n\n## 完整示例（方式 B：搜索用户）\n\n```\n# 1. 打开抖音\nbrowser action=open profile=openclaw targetUrl=https://www.douyin.com/\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n# 2. 搜索目标用户名\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const input = document.querySelector('input[type=\\\"text\\\"]'); if (!input) return 'input not found'; input.focus(); input.value = '<目标用户名>'; input.dispatchEvent(new Event('input', {bubbles: true})); return 'typed'; }\"}\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const btns = document.querySelectorAll('button'); for (const b of btns) { if ((b.textContent || '').includes('搜索')) { b.click(); return 'search clicked'; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n# 3. 在结果中点击目标主页（选有\"相互关注\"标识的条目）\nbrowser action=snapshot\n# 找到目标条目后点击进入主页\n\n# 4. 点击主页\"私信\"按钮\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const btns = document.querySelectorAll('button'); for (const b of btns) { if ((b.textContent || '').trim() === '私信') { b.click(); return 'clicked'; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 800}\n\n# 5. 粘贴 + 发送（与方式 A 相同）\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; (editor.parentElement || editor).focus(); navigator.clipboard.writeText('你好').then(() => document.execCommand('paste')); return 'ok'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 300}\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); editor.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true})); return 'sent'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 1000}\n```\n\nFile v1.1.6:_meta.json\n\n{\n  \"ownerId\": \"kn75k53tcdhqwka2fra5x817p9821zpb\",\n  \"slug\": \"douyin-messager\",\n  \"version\": \"1.1.6\",\n  \"publishedAt\": 1777480584831\n}\n\nArchive v1.1.5: 2 files, 2831 bytes\n\nFiles: _meta.json (134b), SKILL.md (8238b)\n\nFile v1.1.5:SKILL.md\n\n---\nname: Douyin Messager | 抖音私信助手\ndescription: \"Send Douyin DMs, reply, and check chat history through browser automation. 自动发送抖音私信、回复消息、查看聊天记录。\"\n---\n\n# Douyin Private Messaging | 抖音私信发送\n\n通过浏览器自动化发送抖音私信、获取聊天记录。\n\n## ⚠️ 执行前必须确认\n\n1. **用户已登录抖音账号**（通常已登录，可目测判断）\n\n**xdg-open 弹窗：只在 Linux 下存在，Windows/macOS 不需要检查。**\n- Windows：`shell=powershell` 或 `os=Windows_NT`，**跳过 xdg-open 检查**\n- Linux：`shell=bash`，**询问用户是否关闭了弹窗**\n\n---\n\n## 发送私信流程\n\n### 步骤 1：打开抖音\n\n```\nbrowser action=open profile=openclaw targetUrl=https://www.douyin.com/\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n```\n\n### 步骤 2：打开私信面板\n\n**方式 A：置顶联系人（最快）**\n1. 获取快照找到私信按钮，点击打开私信面板\n2. 在私信列表中找目标用户名（注意：列表只显示最近联系人，不滚动看不到中间部分）\n3. 点击目标 → 打开聊天窗口\n\n**方式 B：搜索用户（普适）**\n1. 点击顶栏搜索框，输入用户名\n2. 在搜索结果中找到目标（优先选有\"相互关注\"标识的），点击进入主页\n3. 点击主页的\"私信\"按钮 → 私信面板自动打开\n\n> **注意**：私信面板是动态浮层，每次快照后元素 ref 会变化。优先用 JS evaluate 定位元素，少用 ref 点击。\n\n### 步骤 3：用 JS 操作 Draft.js 输入框\n\n**私信输入框是 Draft.js 富文本编辑器（`contenteditable`），不是普通 input，不能用 `type` 指令直接操作。**\n\n**第一步：找到编辑器**\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = document.querySelectorAll('*'); let panel = null; for (const el of all) { const r = el.getBoundingClientRect(); if (r.width >= 300 && r.width <= 400 && r.height >= 400 && r.height <= 700 && r.left >= 870 && r.left <= 1010) { if (el.textContent.includes('<目标用户名>') && el.textContent.includes('发送消息')) { panel = el; break; } } } if (!panel) return 'panel not found'; const editor = panel.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; return 'found'; }\"}\n```\n\n如果返回 `panel not found`，说明私信面板未打开，重新执行步骤 2。\n\n**第二步：用剪贴板粘贴（绕过 Draft.js 事件拦截）**\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; (editor.parentElement || editor).focus(); navigator.clipboard.writeText('消息内容').then(() => document.execCommand('paste')); return 'ok'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 300}\n```\n\n**第三步：确认已写入**\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; return editor.textContent.includes('消息内容') ? 'ok' : 'not written'; }\"}\n```\n\n**第四步：发送（Enter 键）**\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; editor.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true})); return 'sent'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 1000}\n```\n\n### 步骤 4：确认发送成功\n\n获取快照，检查私信列表中是否出现 `消息内容 · 刚刚`。\n\n---\n\n## 获取对方回复\n\n### 滚动到底部\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const panel = document.querySelector('[class*=\\\"w5duGc5Q\\\"], [class*=\\\"RoMuFUzT\\\"]'); if (!panel) return 'panel not found'; panel.scrollTop = panel.scrollHeight; return 'scrolled'; }\"}\n```\n\n### 截图确认\n\n```\nbrowser action=screenshot\n```\n\n新消息气泡在可视区域底部之外，先滚动再截图。\n\n---\n\n## 完整示例（方式 A：置顶联系人）\n\n```\n# 1. 打开抖音\nbrowser action=open profile=openclaw targetUrl=https://www.douyin.com/\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n# 2. 打开私信面板（JS 点击私信按钮）\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = document.querySelectorAll('*'); for (const el of all) { if ((el.textContent || '').trim() === '私信' && el.getBoundingClientRect().width < 100) { el.click(); return 'clicked'; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 800}\n\n# 3. 在私信列表中点击目标\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = document.querySelectorAll('[class*=\\\"w5duGc5Q\\\"], [class*=\\\"RoMuFUzT\\\"]'); for (const p of all) { const r = p.getBoundingClientRect(); if (r.width === 500 && Math.abs(r.left - 1006) < 50) { const items = p.querySelectorAll('[class*=\\\"cursor-pointer\\\"]'); for (const item of items) { if ((item.textContent || '').includes('<目标用户名>')) { item.click(); return 'clicked target'; } } } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 800}\n\n# 4. 粘贴消息\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; (editor.parentElement || editor).focus(); navigator.clipboard.writeText('你好').then(() => document.execCommand('paste')); return 'ok'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 300}\n\n# 5. 发送\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); editor.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true})); return 'sent'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 1000}\n\n# 6. 确认\nbrowser action=snapshot\n```\n\n---\n\n## 完整示例（方式 B：搜索用户）\n\n```\n# 1. 打开抖音\nbrowser action=open profile=openclaw targetUrl=https://www.douyin.com/\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n# 2. 搜索目标用户名\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const input = document.querySelector('input[type=\\\"text\\\"]'); if (!input) return 'input not found'; input.focus(); input.value = '<目标用户名>'; input.dispatchEvent(new Event('input', {bubbles: true})); return 'typed'; }\"}\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const btns = document.querySelectorAll('button'); for (const b of btns) { if ((b.textContent || '').includes('搜索')) { b.click(); return 'search clicked'; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n# 3. 在结果中点击目标主页（选有\"相互关注\"标识的条目）\nbrowser action=snapshot\n# 找到目标条目后点击进入主页\n\n# 4. 点击主页\"私信\"按钮\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const btns = document.querySelectorAll('button'); for (const b of btns) { if ((b.textContent || '').trim() === '私信') { b.click(); return 'clicked'; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 800}\n\n# 5. 粘贴 + 发送（与方式 A 相同）\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; (editor.parentElement || editor).focus(); navigator.clipboard.writeText('你好').then(() => document.execCommand('paste')); return 'ok'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 300}\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); editor.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true})); return 'sent'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 1000}\n```\n\nFile v1.1.5:_meta.json\n\n{\n  \"ownerId\": \"kn75k53tcdhqwka2fra5x817p9821zpb\",\n  \"slug\": \"douyin-messager\",\n  \"version\": \"1.1.5\",\n  \"publishedAt\": 1775481105579\n}\n\nArchive v1.1.4: 2 files, 2825 bytes\n\nFiles: _meta.json (134b), SKILL.md (8217b)\n\nFile v1.1.4:SKILL.md\n\n---\nname: douyin-messager\ndescription: \"Send Douyin DMs, reply, and check chat history through browser automation. 自动发送抖音私信、回复消息、查看聊天记录。\"\n---\n\n# Douyin Private Messaging | 抖音私信发送\n\n通过浏览器自动化发送抖音私信、获取聊天记录。\n\n## ⚠️ 执行前必须确认\n\n1. **用户已登录抖音账号**（通常已登录，可目测判断）\n\n**xdg-open 弹窗：只在 Linux 下存在，Windows/macOS 不需要检查。**\n- Windows：`shell=powershell` 或 `os=Windows_NT`，**跳过 xdg-open 检查**\n- Linux：`shell=bash`，**询问用户是否关闭了弹窗**\n\n---\n\n## 发送私信流程\n\n### 步骤 1：打开抖音\n\n```\nbrowser action=open profile=openclaw targetUrl=https://www.douyin.com/\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n```\n\n### 步骤 2：打开私信面板\n\n**方式 A：置顶联系人（最快）**\n1. 获取快照找到私信按钮，点击打开私信面板\n2. 在私信列表中找目标用户名（注意：列表只显示最近联系人，不滚动看不到中间部分）\n3. 点击目标 → 打开聊天窗口\n\n**方式 B：搜索用户（普适）**\n1. 点击顶栏搜索框，输入用户名\n2. 在搜索结果中找到目标（优先选有\"相互关注\"标识的），点击进入主页\n3. 点击主页的\"私信\"按钮 → 私信面板自动打开\n\n> **注意**：私信面板是动态浮层，每次快照后元素 ref 会变化。优先用 JS evaluate 定位元素，少用 ref 点击。\n\n### 步骤 3：用 JS 操作 Draft.js 输入框\n\n**私信输入框是 Draft.js 富文本编辑器（`contenteditable`），不是普通 input，不能用 `type` 指令直接操作。**\n\n**第一步：找到编辑器**\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = document.querySelectorAll('*'); let panel = null; for (const el of all) { const r = el.getBoundingClientRect(); if (r.width >= 300 && r.width <= 400 && r.height >= 400 && r.height <= 700 && r.left >= 870 && r.left <= 1010) { if (el.textContent.includes('<目标用户名>') && el.textContent.includes('发送消息')) { panel = el; break; } } } if (!panel) return 'panel not found'; const editor = panel.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; return 'found'; }\"}\n```\n\n如果返回 `panel not found`，说明私信面板未打开，重新执行步骤 2。\n\n**第二步：用剪贴板粘贴（绕过 Draft.js 事件拦截）**\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; (editor.parentElement || editor).focus(); navigator.clipboard.writeText('消息内容').then(() => document.execCommand('paste')); return 'ok'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 300}\n```\n\n**第三步：确认已写入**\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; return editor.textContent.includes('消息内容') ? 'ok' : 'not written'; }\"}\n```\n\n**第四步：发送（Enter 键）**\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; editor.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true})); return 'sent'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 1000}\n```\n\n### 步骤 4：确认发送成功\n\n获取快照，检查私信列表中是否出现 `消息内容 · 刚刚`。\n\n---\n\n## 获取对方回复\n\n### 滚动到底部\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const panel = document.querySelector('[class*=\\\"w5duGc5Q\\\"], [class*=\\\"RoMuFUzT\\\"]'); if (!panel) return 'panel not found'; panel.scrollTop = panel.scrollHeight; return 'scrolled'; }\"}\n```\n\n### 截图确认\n\n```\nbrowser action=screenshot\n```\n\n新消息气泡在可视区域底部之外，先滚动再截图。\n\n---\n\n## 完整示例（方式 A：置顶联系人）\n\n```\n# 1. 打开抖音\nbrowser action=open profile=openclaw targetUrl=https://www.douyin.com/\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n# 2. 打开私信面板（JS 点击私信按钮）\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = document.querySelectorAll('*'); for (const el of all) { if ((el.textContent || '').trim() === '私信' && el.getBoundingClientRect().width < 100) { el.click(); return 'clicked'; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 800}\n\n# 3. 在私信列表中点击目标\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = document.querySelectorAll('[class*=\\\"w5duGc5Q\\\"], [class*=\\\"RoMuFUzT\\\"]'); for (const p of all) { const r = p.getBoundingClientRect(); if (r.width === 500 && Math.abs(r.left - 1006) < 50) { const items = p.querySelectorAll('[class*=\\\"cursor-pointer\\\"]'); for (const item of items) { if ((item.textContent || '').includes('<目标用户名>')) { item.click(); return 'clicked target'; } } } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 800}\n\n# 4. 粘贴消息\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; (editor.parentElement || editor).focus(); navigator.clipboard.writeText('你好').then(() => document.execCommand('paste')); return 'ok'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 300}\n\n# 5. 发送\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); editor.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true})); return 'sent'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 1000}\n\n# 6. 确认\nbrowser action=snapshot\n```\n\n---\n\n## 完整示例（方式 B：搜索用户）\n\n```\n# 1. 打开抖音\nbrowser action=open profile=openclaw targetUrl=https://www.douyin.com/\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n# 2. 搜索目标用户名\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const input = document.querySelector('input[type=\\\"text\\\"]'); if (!input) return 'input not found'; input.focus(); input.value = '<目标用户名>'; input.dispatchEvent(new Event('input', {bubbles: true})); return 'typed'; }\"}\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const btns = document.querySelectorAll('button'); for (const b of btns) { if ((b.textContent || '').includes('搜索')) { b.click(); return 'search clicked'; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n# 3. 在结果中点击目标主页（选有\"相互关注\"标识的条目）\nbrowser action=snapshot\n# 找到目标条目后点击进入主页\n\n# 4. 点击主页\"私信\"按钮\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const btns = document.querySelectorAll('button'); for (const b of btns) { if ((b.textContent || '').trim() === '私信') { b.click(); return 'clicked'; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 800}\n\n# 5. 粘贴 + 发送（与方式 A 相同）\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; (editor.parentElement || editor).focus(); navigator.clipboard.writeText('你好').then(() => document.execCommand('paste')); return 'ok'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 300}\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); editor.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true})); return 'sent'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 1000}\n```\n\nFile v1.1.4:_meta.json\n\n{\n  \"ownerId\": \"kn75k53tcdhqwka2fra5x817p9821zpb\",\n  \"slug\": \"douyin-messager\",\n  \"version\": \"1.1.4\",\n  \"publishedAt\": 1775446386776\n}\n\nArchive v1.1.3: 2 files, 2825 bytes\n\nFiles: _meta.json (134b), SKILL.md (8217b)\n\nFile v1.1.3:SKILL.md\n\n---\nname: douyin-messager\ndescription: \"Send Douyin DMs, reply, and check chat history through browser automation. 自动发送抖音私信、回复消息、查看聊天记录。\"\n---\n\n# Douyin Private Messaging | 抖音私信发送\n\n通过浏览器自动化发送抖音私信、获取聊天记录。\n\n## ⚠️ 执行前必须确认\n\n1. **用户已登录抖音账号**（通常已登录，可目测判断）\n\n**xdg-open 弹窗：只在 Linux 下存在，Windows/macOS 不需要检查。**\n- Windows：`shell=powershell` 或 `os=Windows_NT`，**跳过 xdg-open 检查**\n- Linux：`shell=bash`，**询问用户是否关闭了弹窗**\n\n---\n\n## 发送私信流程\n\n### 步骤 1：打开抖音\n\n```\nbrowser action=open profile=openclaw targetUrl=https://www.douyin.com/\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n```\n\n### 步骤 2：打开私信面板\n\n**方式 A：置顶联系人（最快）**\n1. 获取快照找到私信按钮，点击打开私信面板\n2. 在私信列表中找目标用户名（注意：列表只显示最近联系人，不滚动看不到中间部分）\n3. 点击目标 → 打开聊天窗口\n\n**方式 B：搜索用户（普适）**\n1. 点击顶栏搜索框，输入用户名\n2. 在搜索结果中找到目标（优先选有\"相互关注\"标识的），点击进入主页\n3. 点击主页的\"私信\"按钮 → 私信面板自动打开\n\n> **注意**：私信面板是动态浮层，每次快照后元素 ref 会变化。优先用 JS evaluate 定位元素，少用 ref 点击。\n\n### 步骤 3：用 JS 操作 Draft.js 输入框\n\n**私信输入框是 Draft.js 富文本编辑器（`contenteditable`），不是普通 input，不能用 `type` 指令直接操作。**\n\n**第一步：找到编辑器**\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = document.querySelectorAll('*'); let panel = null; for (const el of all) { const r = el.getBoundingClientRect(); if (r.width >= 300 && r.width <= 400 && r.height >= 400 && r.height <= 700 && r.left >= 870 && r.left <= 1010) { if (el.textContent.includes('<目标用户名>') && el.textContent.includes('发送消息')) { panel = el; break; } } } if (!panel) return 'panel not found'; const editor = panel.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; return 'found'; }\"}\n```\n\n如果返回 `panel not found`，说明私信面板未打开，重新执行步骤 2。\n\n**第二步：用剪贴板粘贴（绕过 Draft.js 事件拦截）**\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; (editor.parentElement || editor).focus(); navigator.clipboard.writeText('消息内容').then(() => document.execCommand('paste')); return 'ok'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 300}\n```\n\n**第三步：确认已写入**\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; return editor.textContent.includes('消息内容') ? 'ok' : 'not written'; }\"}\n```\n\n**第四步：发送（Enter 键）**\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; editor.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true})); return 'sent'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 1000}\n```\n\n### 步骤 4：确认发送成功\n\n获取快照，检查私信列表中是否出现 `消息内容 · 刚刚`。\n\n---\n\n## 获取对方回复\n\n### 滚动到底部\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const panel = document.querySelector('[class*=\\\"w5duGc5Q\\\"], [class*=\\\"RoMuFUzT\\\"]'); if (!panel) return 'panel not found'; panel.scrollTop = panel.scrollHeight; return 'scrolled'; }\"}\n```\n\n### 截图确认\n\n```\nbrowser action=screenshot\n```\n\n新消息气泡在可视区域底部之外，先滚动再截图。\n\n---\n\n## 完整示例（方式 A：置顶联系人）\n\n```\n# 1. 打开抖音\nbrowser action=open profile=openclaw targetUrl=https://www.douyin.com/\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n# 2. 打开私信面板（JS 点击私信按钮）\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = document.querySelectorAll('*'); for (const el of all) { if ((el.textContent || '').trim() === '私信' && el.getBoundingClientRect().width < 100) { el.click(); return 'clicked'; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 800}\n\n# 3. 在私信列表中点击目标\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = document.querySelectorAll('[class*=\\\"w5duGc5Q\\\"], [class*=\\\"RoMuFUzT\\\"]'); for (const p of all) { const r = p.getBoundingClientRect(); if (r.width === 500 && Math.abs(r.left - 1006) < 50) { const items = p.querySelectorAll('[class*=\\\"cursor-pointer\\\"]'); for (const item of items) { if ((item.textContent || '').includes('<目标用户名>')) { item.click(); return 'clicked target'; } } } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 800}\n\n# 4. 粘贴消息\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; (editor.parentElement || editor).focus(); navigator.clipboard.writeText('你好').then(() => document.execCommand('paste')); return 'ok'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 300}\n\n# 5. 发送\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); editor.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true})); return 'sent'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 1000}\n\n# 6. 确认\nbrowser action=snapshot\n```\n\n---\n\n## 完整示例（方式 B：搜索用户）\n\n```\n# 1. 打开抖音\nbrowser action=open profile=openclaw targetUrl=https://www.douyin.com/\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n# 2. 搜索目标用户名\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const input = document.querySelector('input[type=\\\"text\\\"]'); if (!input) return 'input not found'; input.focus(); input.value = '<目标用户名>'; input.dispatchEvent(new Event('input', {bubbles: true})); return 'typed'; }\"}\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const btns = document.querySelectorAll('button'); for (const b of btns) { if ((b.textContent || '').includes('搜索')) { b.click(); return 'search clicked'; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n# 3. 在结果中点击目标主页（选有\"相互关注\"标识的条目）\nbrowser action=snapshot\n# 找到目标条目后点击进入主页\n\n# 4. 点击主页\"私信\"按钮\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const btns = document.querySelectorAll('button'); for (const b of btns) { if ((b.textContent || '').trim() === '私信') { b.click(); return 'clicked'; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 800}\n\n# 5. 粘贴 + 发送（与方式 A 相同）\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); if (!editor) return 'editor not found'; (editor.parentElement || editor).focus(); navigator.clipboard.writeText('你好').then(() => document.execCommand('paste')); return 'ok'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 300}\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const editor = document.querySelector('.public-DraftEditor-content'); editor.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true})); return 'sent'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 1000}\n```\n\nFile v1.1.3:_meta.json\n\n{\n  \"ownerId\": \"kn75k53tcdhqwka2fra5x817p9821zpb\",\n  \"slug\": \"douyin-messager\",\n  \"version\": \"1.1.3\",\n  \"publishedAt\": 1774344019250\n}\n\nArchive v1.1.2: 2 files, 3185 bytes\n\nFiles: SKILL.md (6774b), _meta.json (134b)\n\nFile v1.1.2:SKILL.md\n\n---\nname: douyin-messager\ndescription: \"Douyin private messaging via browser automation. 抖音私信发送，通过浏览器自动化发送私信、获取聊天记录。Use when user needs to send 抖音私信、回复消息、查看聊天记录。Requires browser login. Triggers: 抖音, douyin, Douyin, TikTok中国, 抖音私信, douyin dm, douyin message, 抖音消息, 抖音发消息, send douyin message, 抖音聊天, douyin chat, 抖音回复, reply douyin, 获取抖音聊天记录, get douyin chat history, 抖音私信发送, send douyin dm, 给抖音用户发消息, message douyin user, 抖音私信自动化, douyin dm automation, 抖音私信工具, douyin messaging tool.\"\n---\n\n# 抖音私信发送 | Douyin Private Messaging\n\n通过浏览器自动化发送抖音私信、获取聊天记录。\n\n## ⚠️ 最大前提（必须满足）\n\n**在执行任何操作之前，必须确认以下两点：**\n\n1. **用户已登录抖音账号**\n2. **用户已关闭 xdg-open 弹窗**（如果有）\n\n### 标准执行流程\n\n```\n1. 打开抖音\n2. 等待 2-3 秒\n3. 【必须】询问用户：\"请确认：1) 已登录抖音账号  2) 已关闭 xdg-open 弹窗\"\n4. 用户确认后，继续后续操作\n```\n\n---\n\n## ⚠️ xdg-open 系统弹窗\n\n### 问题描述\n\n打开抖音主页时，会触发 Linux 系统级的 `xdg-open` 弹窗：\n\n```\nOpen xdg-open?\nhttps://www.douyin.com wants to open this application.\n[✓] Always allow www.douyin.com to open links of this type in the associated app\n[Cancel] [Open]\n```\n\n### 关键点\n\n| 特性 | 说明 |\n|------|------|\n| 弹窗类型 | Linux 系统级，非浏览器内 |\n| AI 可见性 | ❌ 截图看不到 |\n| AI 可操作性 | ❌ 无法操作 |\n| 阻塞性 | ✅ 阻止所有浏览器操作 |\n| 永久关闭 | ❌ 目前无方法 |\n\n### 解决方案\n\n**必须由用户手动关闭**：点击 **Cancel**（不要点 Open）\n\n### AI 检测方法\n\n如果出现以下情况，询问用户是否有弹窗：\n- 操作突然无响应\n- 截图显示页面正常但操作不执行\n- 点击后没有反应\n\n---\n\n## 核心要点\n\n1. **发送方式**：必须用 `type + submit`，不要用 JavaScript 操作 DOM\n2. **减少快照**：快照数据量大，容易超时或断网\n3. **输入框 ref**：每次页面加载都会变化，需要重新获取快照\n4. **连续执行**：不要中途停止\n\n---\n\n## 发送私信流程\n\n### 步骤 1：打开抖音首页\n\n```\nbrowser action=open profile=openclaw targetUrl=https://www.douyin.com/\n```\n\n### 步骤 2：等待页面加载\n\n```\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n```\n\n### 步骤 3：【必须】确认用户状态\n\n**询问用户**：\"请确认：1) 已登录抖音账号  2) 已关闭 xdg-open 弹窗\"\n\n用户确认后继续。\n\n### 步骤 4：获取快照，找到私信按钮和目标用户\n\n```\nbrowser action=snapshot\n```\n\n**私信按钮特征**：\n```\n- generic [cursor=pointer]:\n    - img\n    - paragraph: \"私信\"\n```\n\n**用户列表特征**：\n```\n- generic [cursor=pointer]:  <- 用户项\n    - generic: \"井媛\"\n    - generic: \"置顶\"\n```\n\n### 步骤 5：点击私信按钮\n\n```\nbrowser action=act request={\"kind\": \"click\", \"ref\": \"<私信按钮ref>\"}\n```\n\n### 步骤 6：点击目标用户\n\n```\nbrowser action=act request={\"kind\": \"click\", \"ref\": \"<用户项ref>\"}\n```\n\n### 步骤 7：等待 + 获取快照\n\n```\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 500}\nbrowser action=snapshot\n```\n\n**输入框特征**：\n```\ntextbox [active] [ref=e3963]\n```\n\n### 步骤 8：发送消息\n\n**✅ 正确方法**：\n```\nbrowser action=act request={\"kind\": \"type\", \"ref\": \"<输入框ref>\", \"text\": \"消息内容\", \"submit\": true}\n```\n\n**❌ 错误方法**（会导致乱码）：\n```\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { document.querySelector('...')... }\"}\n```\n\n### 步骤 9：确认发送成功\n\n```\nbrowser action=screenshot\n```\n\n用 image 工具分析截图，确认消息出现在聊天记录中（蓝色气泡）。\n\n---\n\n## 获取对方回复\n\n### 步骤 1：滚动聊天区域到底部\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => {\n  const containers = document.querySelectorAll('[class*=\\\"scroll\\\"]');\n  containers.forEach(c => { c.scrollTop = c.scrollHeight; });\n  return 'Scrolled';\n}\"}\n```\n\n### 步骤 2：截图或快照\n\n```\nbrowser action=screenshot\n```\n\n或\n\n```\nbrowser action=snapshot\n```\n\n### 消息特征\n\n| 类型 | 特征 |\n|------|------|\n| 自己发送 | 蓝色气泡，右侧，显示时间 |\n| 对方发送 | 白色/灰色气泡，左侧，显示头像和名字 |\n\n---\n\n## 完整示例\n\n```\n# 1. 打开抖音\nbrowser action=open profile=openclaw targetUrl=https://www.douyin.com/\n\n# 2. 等待加载\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n# 3. 【必须】询问用户确认登录状态和弹窗\n\n# 4. 获取快照\nbrowser action=snapshot\n\n# 5. 点击私信按钮\nbrowser action=act request={\"kind\": \"click\", \"ref\": \"e255\"}\n\n# 6. 点击目标用户\nbrowser action=act request={\"kind\": \"click\", \"ref\": \"e2215\"}\n\n# 7. 等待 + 获取快照\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 500}\nbrowser action=snapshot\n\n# 8. 发送消息\nbrowser action=act request={\"kind\": \"type\", \"ref\": \"e3963\", \"text\": \"你好！\", \"submit\": true}\n\n# 9. 确认\nbrowser action=screenshot\n```\n\n---\n\n## 常见问题\n\n### Q: 消息发送后显示乱码？\n\nA: 不要用 JavaScript 操作 DOM。用 \\`type + submit\\`\n\n### Q: 看不到对方回复？\n\nA: 新消息可能在可视区域外。**先滚动到底部**，再截图\n\n### Q: 输入框 ref 找不到或超时？\n\nA: 输入框 ref 每次页面加载都会变化。重新获取快照查找 \\`textbox [active]\\`\n\n### Q: 私信面板找不到？\n\nA: 确保已登录抖音账号。未登录时私信按钮不会显示\n\n### Q: 操作无响应？\n\nA: 可能是 xdg-open 弹窗阻挡。询问用户是否有系统弹窗需要关闭\n\n### Q: 快照获取失败或断网？\n\nA: 快照数据量大，可能超时。减少快照次数，只在必要时获取\n\n---\n\n## 注意事项\n\n- **消息长度限制**：500字符（超过需分段发送）\n- **发送频率限制**：避免刷屏\n- **私信面板是悬浮窗**：不会跳转页面\n- **不要硬编码 DOM class 哈希值**：它们会变化，用文本内容或相对位置判断\n\n---\n\n## 经验总结\n\n| 问题 | 原因 | 解决方案 |\n|------|------|----------|\n| 消息乱码 | 用 JS 操作 DOM | 用 type + submit |\n| 看不到回复 | 新消息在可视区域外 | 先滚动到底部 |\n| 输入框超时 | ref 每次变化 | 重新获取快照 |\n| 操作无响应 | xdg-open 弹窗 | 用户手动关闭 |\n| 快照断网 | 数据量大 | 减少快照次数 |\n\n---\n\n*抖音私信自动化 🎵*\n*版本：1.0.0*\n*最后更新：2026-03-02*\n\nFile v1.1.2:_meta.json\n\n{\n  \"ownerId\": \"kn75k53tcdhqwka2fra5x817p9821zpb\",\n  \"slug\": \"douyin-messager\",\n  \"version\": \"1.1.2\",\n  \"publishedAt\": 1773339880568\n}\n\nArchive v1.1.1: 2 files, 3243 bytes\n\nFiles: SKILL.md (6861b), _meta.json (134b)\n\nFile v1.1.1:SKILL.md\n\n---\nname: douyin-messager\ndescription: \"Douyin private messaging via browser automation. 抖音私信发送，通过浏览器自动化发送私信、获取聊天记录。Use when user needs to send 抖音私信、回复消息、查看聊天记录。Requires browser login. Triggers: 抖音, douyin, Douyin, TikTok中国, 抖音私信, douyin dm, douyin message, 抖音消息, 抖音发消息, send douyin message, 抖音聊天, douyin chat, 抖音回复, reply douyin, 获取抖音聊天记录, get douyin chat history, 抖音私信发送, send douyin dm, 给抖音用户发消息, message douyin user, 抖音私信自动化, douyin dm automation, 抖音私信工具, douyin messaging tool.\"\nmetadata: {\"openclaw\": {\"emoji\": \"🎵\", \"requires\": {\"config\": [\"browser.enabled\"]}}}\n---\n\n# 抖音私信发送 | Douyin Private Messaging\n\n通过浏览器自动化发送抖音私信、获取聊天记录。\n\n## ⚠️ 最大前提（必须满足）\n\n**在执行任何操作之前，必须确认以下两点：**\n\n1. **用户已登录抖音账号**\n2. **用户已关闭 xdg-open 弹窗**（如果有）\n\n### 标准执行流程\n\n```\n1. 打开抖音\n2. 等待 2-3 秒\n3. 【必须】询问用户：\"请确认：1) 已登录抖音账号  2) 已关闭 xdg-open 弹窗\"\n4. 用户确认后，继续后续操作\n```\n\n---\n\n## ⚠️ xdg-open 系统弹窗\n\n### 问题描述\n\n打开抖音主页时，会触发 Linux 系统级的 `xdg-open` 弹窗：\n\n```\nOpen xdg-open?\nhttps://www.douyin.com wants to open this application.\n[✓] Always allow www.douyin.com to open links of this type in the associated app\n[Cancel] [Open]\n```\n\n### 关键点\n\n| 特性 | 说明 |\n|------|------|\n| 弹窗类型 | Linux 系统级，非浏览器内 |\n| AI 可见性 | ❌ 截图看不到 |\n| AI 可操作性 | ❌ 无法操作 |\n| 阻塞性 | ✅ 阻止所有浏览器操作 |\n| 永久关闭 | ❌ 目前无方法 |\n\n### 解决方案\n\n**必须由用户手动关闭**：点击 **Cancel**（不要点 Open）\n\n### AI 检测方法\n\n如果出现以下情况，询问用户是否有弹窗：\n- 操作突然无响应\n- 截图显示页面正常但操作不执行\n- 点击后没有反应\n\n---\n\n## 核心要点\n\n1. **发送方式**：必须用 `type + submit`，不要用 JavaScript 操作 DOM\n2. **减少快照**：快照数据量大，容易超时或断网\n3. **输入框 ref**：每次页面加载都会变化，需要重新获取快照\n4. **连续执行**：不要中途停止\n\n---\n\n## 发送私信流程\n\n### 步骤 1：打开抖音首页\n\n```\nbrowser action=open profile=openclaw targetUrl=https://www.douyin.com/\n```\n\n### 步骤 2：等待页面加载\n\n```\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n```\n\n### 步骤 3：【必须】确认用户状态\n\n**询问用户**：\"请确认：1) 已登录抖音账号  2) 已关闭 xdg-open 弹窗\"\n\n用户确认后继续。\n\n### 步骤 4：获取快照，找到私信按钮和目标用户\n\n```\nbrowser action=snapshot\n```\n\n**私信按钮特征**：\n```\n- generic [cursor=pointer]:\n    - img\n    - paragraph: \"私信\"\n```\n\n**用户列表特征**：\n```\n- generic [cursor=pointer]:  <- 用户项\n    - generic: \"井媛\"\n    - generic: \"置顶\"\n```\n\n### 步骤 5：点击私信按钮\n\n```\nbrowser action=act request={\"kind\": \"click\", \"ref\": \"<私信按钮ref>\"}\n```\n\n### 步骤 6：点击目标用户\n\n```\nbrowser action=act request={\"kind\": \"click\", \"ref\": \"<用户项ref>\"}\n```\n\n### 步骤 7：等待 + 获取快照\n\n```\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 500}\nbrowser action=snapshot\n```\n\n**输入框特征**：\n```\ntextbox [active] [ref=e3963]\n```\n\n### 步骤 8：发送消息\n\n**✅ 正确方法**：\n```\nbrowser action=act request={\"kind\": \"type\", \"ref\": \"<输入框ref>\", \"text\": \"消息内容\", \"submit\": true}\n```\n\n**❌ 错误方法**（会导致乱码）：\n```\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { document.querySelector('...')... }\"}\n```\n\n### 步骤 9：确认发送成功\n\n```\nbrowser action=screenshot\n```\n\n用 image 工具分析截图，确认消息出现在聊天记录中（蓝色气泡）。\n\n---\n\n## 获取对方回复\n\n### 步骤 1：滚动聊天区域到底部\n\n```javascript\nbrowser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => {\n  const containers = document.querySelectorAll('[class*=\\\"scroll\\\"]');\n  containers.forEach(c => { c.scrollTop = c.scrollHeight; });\n  return 'Scrolled';\n}\"}\n```\n\n### 步骤 2：截图或快照\n\n```\nbrowser action=screenshot\n```\n\n或\n\n```\nbrowser action=snapshot\n```\n\n### 消息特征\n\n| 类型 | 特征 |\n|------|------|\n| 自己发送 | 蓝色气泡，右侧，显示时间 |\n| 对方发送 | 白色/灰色气泡，左侧，显示头像和名字 |\n\n---\n\n## 完整示例\n\n```\n# 1. 打开抖音\nbrowser action=open profile=openclaw targetUrl=https://www.douyin.com/\n\n# 2. 等待加载\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}\n\n# 3. 【必须】询问用户确认登录状态和弹窗\n\n# 4. 获取快照\nbrowser action=snapshot\n\n# 5. 点击私信按钮\nbrowser action=act request={\"kind\": \"click\", \"ref\": \"e255\"}\n\n# 6. 点击目标用户\nbrowser action=act request={\"kind\": \"click\", \"ref\": \"e2215\"}\n\n# 7. 等待 + 获取快照\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 500}\nbrowser action=snapshot\n\n# 8. 发送消息\nbrowser action=act request={\"kind\": \"type\", \"ref\": \"e3963\", \"text\": \"你好！\", \"submit\": true}\n\n# 9. 确认\nbrowser action=screenshot\n```\n\n---\n\n## 常见问题\n\n### Q: 消息发送后显示乱码？\n\nA: 不要用 JavaScript 操作 DOM。用 \\`type + submit\\`\n\n### Q: 看不到对方回复？\n\nA: 新消息可能在可视区域外。**先滚动到底部**，再截图\n\n### Q: 输入框 ref 找不到或超时？\n\nA: 输入框 ref 每次页面加载都会变化。重新获取快照查找 \\`textbox [active]\\`\n\n### Q: 私信面板找不到？\n\nA: 确保已登录抖音账号。未登录时私信按钮不会显示\n\n### Q: 操作无响应？\n\nA: 可能是 xdg-open 弹窗阻挡。询问用户是否有系统弹窗需要关闭\n\n### Q: 快照获取失败或断网？\n\nA: 快照数据量大，可能超时。减少快照次数，只在必要时获取\n\n---\n\n## 注意事项\n\n- **消息长度限制**：500字符（超过需分段发送）\n- **发送频率限制**：避免刷屏\n- **私信面板是悬浮窗**：不会跳转页面\n- **不要硬编码 DOM class 哈希值**：它们会变化，用文本内容或相对位置判断\n\n---\n\n## 经验总结\n\n| 问题 | 原因 | 解决方案 |\n|------|------|----------|\n| 消息乱码 | 用 JS 操作 DOM | 用 type + submit |\n| 看不到回复 | 新消息在可视区域外 | 先滚动到底部 |\n| 输入框超时 | ref 每次变化 | 重新获取快照 |\n| 操作无响应 | xdg-open 弹窗 | 用户手动关闭 |\n| 快照断网 | 数据量大 | 减少快照次数 |\n\n---\n\n*抖音私信自动化 🎵*\n*版本：1.0.0*\n*最后更新：2026-03-02*\n\nFile v1.1.1:_meta.json\n\n{\n  \"ownerId\": \"kn75k53tcdhqwka2fra5x817p9821zpb\",\n  \"slug\": \"douyin-messager\",\n  \"version\": \"1.1.1\",\n  \"publishedAt\": 1772481326133\n}","readmeExcerpt":"Skill: Douyin Messager | 抖音私信助手 Owner: moroiser Summary: Douyin DMs and video/note comment assistant. 抖音私信与视频/图文评论助手；可读私信、分析评论区，评论/回复等写入前必须确认。 Tags: latest:1.2.0 Version history: v1.2.0 | 2026-05-04T17:25:39.073Z | user 1. Added: credentialExpectations, filesystemWrites, and warning metadata for ClawScan. 新增：凭据声明、文件写入范围和警告元数据，适配 ClawScan。 v1.1.9 | 2026-05-04T17:05:59.131Z | user 1. Added: Browser-session credential d","codeSnippets":[],"executableExamples":[{"language":"javascript","snippet":"browser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const rows = []; const all = Array.from(document.querySelectorAll('*')); for (const el of all) { const r = el.getBoundingClientRect(); const text = (el.textContent || '').trim(); if (r.width >= 250 && r.width <= 360 && r.height >= 50 && r.height <= 90 && r.left > window.innerWidth * 0.55 && text && !text.includes('私信（')) { rows.push({top: Math.round(r.top), text}); } } return rows.sort((a,b) => a.top - b.top); }\"}"},{"language":"javascript","snippet":"browser action=open profile=openclaw targetUrl=https://www.douyin.com/\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 3000}"},{"language":"javascript","snippet":"browser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const t = (el.textContent||'').trim(); const r = el.getBoundingClientRect(); if((t === '私信' || t.startsWith('私信')) && r.width > 0 && r.height > 0 && r.width < 100 && r.left > window.innerWidth - 200) { el.click(); return 'clicked: ' + t; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}"},{"language":"javascript","snippet":"browser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = document.querySelectorAll('[class]'); for(const el of all) { const r = el.getBoundingClientRect(); if(r.width >= 450 && r.width <= 650 && r.height >= 500 && r.height <= 750 && r.left > 0 && r.top > 0 && r.left < 1500) { const text = (el.textContent||'').trim(); if(text.includes('私信')) return 'found panel: ' + r.width + 'x' + r.height + ' at ' + Math.round(r.left); } } return 'panel not found'; }\"}"},{"language":"javascript","snippet":"browser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = Array.from(document.querySelectorAll('*')); for(const el of all) { const t = (el.textContent||'').trim(); const r = el.getBoundingClientRect(); if(t.includes('<目标用户名>') && r.width > 0 && r.height > 0 && r.left > 0) { el.click(); return 'clicked: ' + t; } } return 'not found'; }\"}\nbrowser action=act request={\"kind\": \"wait\", \"timeMs\": 2000}"},{"language":"javascript","snippet":"browser action=act request={\"kind\": \"evaluate\", \"fn\": \"() => { const all = document.querySelectorAll('[contenteditable=\\\"true\\\"]'); for(const el of all) { const r = el.getBoundingClientRect(); if(r.width > 200 && r.width < 500 && r.height > 10 && r.height < 100 && r.left > 1000) return 'found input at ' + Math.round(r.left) + ',' + Math.round(r.top); } return 'input not found'; }\"}"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: Douyin Messager | 抖音私信助手\ndescription: \"Douyin DMs and video/note comment assistant. 抖音私信与视频/图文评论助手；可读私信、分析评论区，评论/回复等写入前必须确认。\"\nmetadata:\n  \"openclaw\":\n    requires:\n      bins: [\"browser\"]\n      browser_profiles: [\"openclaw\"]\n    runtime:\n      credentialExpectations:\n        - \"抖音已登录的浏览器会话（openclaw profile）\"\n      filesystemWrites:\n        - \"~/.openclaw/workspace/skills/douyin-messager/\"\n        - \"~/.openclaw/workspace/projects/douyin-messager/\"\n    warning:\n      - \"发送/评论/回复/点赞等写入操作需要用户确认\"\n      - \"读取聊天记录会将私信内容暴露到 agent 上下文\"\n      - \"使用独立 openclaw profile，不与其他账号混用\"\n---\n\n# Douyin Messager | 抖音私信助手\n\n通过浏览器自动化发送抖音私信、获取聊天记录；也支持打开抖音视频/图文链接、读取评论区并做简要分析，在用户明确确认后执行评论/回复等写入操作。\n\n## 前置要求 | Prerequisites\n\n| 条件 | 说明 |\n|------|------|\n| **Browser profile** | 必须使用 `openclaw` profile |\n| **登录状态** | 抖音账号已在 `openclaw` profile 中保持登录态 |\n\n---\n\n## ⚠️ 执行前必须确认\n\n1. **用户已登录抖音账号**（可目测判断）\n2. **xdg-open 弹窗**：只在 Linux 下存在\n   - Linux：询问用户弹窗是否关闭\n   - Windows/macOS：跳过\n\n---\n\n## ⚠️ 安全声明 | Security Notice\n\n本技能通过已登录的抖音浏览器会话读写私信和评论。\n\n### 浏览器会话凭据声明\n\n本技能依赖已登录的抖音浏览器会话作为账户凭据。使用前请确认：\n\n1. **确认登录账户**：发送操作前，先确认当前登录的是正确的抖音账户\n2. **使用专用 profile**：必须使用独立的 `openclaw` profile，不与日常浏览器混用\n3. **用完后清理**：关闭 `openclaw` profile 的浏览器标签页即可；不再使用时，可清除该 profile 的抖音登录态\n\n### 数据披露\n\n- 读取聊天记录和评论区会将内容暴露到 agent 上下文，仅读取你愿意分享的内容\n- 不要用于读取高度敏感或非自愿的私人对话\n\n### 操作限制\n\n- **发送私信**：必须先向用户确认目标账号和消息内容，获得明确同意后才执行\n- **评论/回复/点赞/分享**：属于外部互动写入操作，必须先获得用户明确确认\n\n---\n\n## 当前能力边界（阶段性总结）\n\n本技能当前已验证可完成四类操作：\n\n1. **查看私信会话列表**：进入私信悬浮面板后，读取可见的私聊/群聊列表，区分会话名称、最新消息预览、时间、未读数和置顶状态。\n2. **进入具体聊天窗口**：从会话列表点击指定私聊或群聊，进入聊天详情页，读取当前已加载的聊天记录。\n3. **发送文本消息**：在聊天详情底部的 Draft.js 输入框中输入文本，并通过发送按钮完成发送。\n4. **视频/图文评论区处理**：可搜索或打开指定视频/图文链接，读取评论区，输出情绪简报；评论、回复、点赞等外部互动写入必须先获得用户明确确认。\n\n需要注意：网页版对视频、图集、点赞、撤回、暂不支持消息等卡片类内容的 DOM 暴露不完整，读取时应保守标注，不应强行推断完整内容或发送者。\n\n---\n\n## 核心思路\n\n> 抖音私信是一个**悬浮在页面右侧的动态面板**，包含\"会话列表\"和\"聊天详情\"两个视图。\n\n**会话列表布局**（类 QQ/微信）：左侧头像，中间分上下两行（上行：用户名/群名；下行：最新消息预览，群聊格式为「发消息的人：消息内容」），右侧时间戳。\n\n**聊天详情布局**：进入某个会话后，右侧显示聊天记录。普通文本消息通常可提取时间、消息内容，并可根据气泡方向、操作项（如是否有「撤回」）辅助判断是否为本人消息；群聊中他人文本消息有时可从消息文本或引用结构中识别发送者。卡片类消息需谨慎处理。\n\n**方法论**：不依赖固定 class name，而是通过元素的**几何特征 + 内容特征**动态查找。具体 class name 仅作参考示例。\n\n---\n\n## 获取私信列表流程\n\n获取私信的第一步不是进入某个聊天，而是**正确进入私信会话列表**。\n\n### 步骤 1：打开私信会话列表\n\n1. 打开抖音主页或个人主页。\n2. 在顶部导航栏右侧定位「私信」入口（可能显示未读数，如「私信11」）。\n3. 点击后等待右侧悬浮面板出现。\n4. 确认当前处于会话列表视图，而不是某个聊天详情页。\n\n### 步骤 2：识别每一行会话\n\n会话列表中每一行对应一个私聊或群聊。布局通常为：左侧头像；中间上行为会话名称（用户名/群名，可能带「置顶」标记）；中间下行为最新消息预览；右侧为时间戳或未读标记。\n\n⚠️ 会话列表本身是可滚动容器，不要只按当前可见区域判断数量。实测中滚动容器宽约 328px、高约 593px，`scrollHeight > clientHeight`，可通过 `scrollTop` 滚到底部；出现「暂时没有更多了」才表示到底。\n\n解析时应优先按**行容器**处理，而不是直接读取父元素的合并文本。读取每行时分别提取：\n\n| 字段 | 识别方法 |\n|------|----------|\n| 会话名称 | 行内上方文本，通常是用户名或群名；若有「置顶」应作为标记处理，不并入名称 |\n| 最新消息 | 行内下方文本；群聊常见格式为「发送者：消息内容」 |\n| 最新发送者 | 若最新消息包含「：」，冒号前通常是群聊中最新消息发送者 |\n| 时间 | 行右侧或下行末尾的时间文本，如「昨天」「04-25」「01:42」 |\n| 未读数 | 行右侧 badge 数字；顶部「私信（N）」通常表示总未读数 |\n| 置顶 | 名称行附近出现「置顶」标记时记录为 pinned=true |\n\n### 步骤 3：区分私聊与群聊\n\n- 私聊：上行通常是对方昵称；下行多为消息内容本身。\n- 群聊：上行是群名；下行常为「发送者：消息内容」。\n- 不要把上行群名和下行发送者合并为一个名称。\n\n**示例参考**：\n\n| 上行（会话名称） | 下行（最新消息） | 解析结果 |\n|------------------|--------"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn75k53tcdhqwka2fra5x817p9821zpb\",\n  \"slug\": \"douyin-messager\",\n  \"version\": \"1.2.0\",\n  \"publishedAt\": 1777915539073\n}"},{"path":"skill-card.md","content":"## Description:\n\nA Douyin DM and video/note comment assistant for reading messages and comments, analyzing comment threads, and performing confirmed comments or replies.\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 and agents use this skill to operate Douyin through a logged-in browser session: review DM conversations, read and summarize visible comments, and draft or perform explicitly confirmed messaging and comment interactions.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Broad page selectors may target the wrong recipient, composer, or content under the user's logged-in Douyin account.\n\nMitigation: Before any send, comment, reply, like, or share action, verify the active account, intended recipient or target content, and exact text with the user.\n\nRisk: Reading DMs or comments exposes private or sensitive Douyin content to the agent context.\n\nMitigation: Use the skill only with messages and comment threads the user is comfortable sharing, and avoid highly sensitive or non-consensual private conversations.\n\nRisk: Using a shared browser profile can mix account credentials or activity with unrelated browsing sessions.\n\nMitigation: Use the dedicated openclaw browser profile and confirm the logged-in account before interactive operations.\n\n## Reference(s):\n\n- [ClawHub skill page](https://clawhub.ai/moroiser/skills/douyin-messager)\n- [Publisher profile](https://clawhub.ai/user/moroiser)\n- [Douyin web app](https://www.douyin.com/)\n- [Douyin video search example](https://www.douyin.com/search/OpenClaw?type=video)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, code, shell commands, configuration, guidance]\n\n**Output Format:** [Markdown guidance with browser automation commands and JavaScript snippets]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Requires a logged-in Douyin browser session using the openclaw profile; write actions require explicit user confirmation.]\n\n## Skill Version(s):\n\n1.2.0 (source: server release evidence)\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":"Douyin DMs and video/note comment assistant. 抖音私信与视频/图文评论助手；可读私信、分析评论区，评论/回复等写入前必须确认。 Skill: Douyin Messager | 抖音私信助手 Owner: moroiser Summary: Douyin DMs and video/note comment assistant. 抖音私信与视频/图文评论助手；可读私信、分析评论区，评论/回复等写入前必须确认。 Tags: latest:1.2.0 Version history: v1.2.0 | 2026-05-04T17:25:39.073Z | user 1. Added: credentialExpectations, filesystemWrites, and warning metadata for ClawScan. 新增：凭据声明、文件写入范围和警告元数据，适配 ClawScan。 v1.1.9 | 2026-05-04T17:05:59.131Z | user 1. Added: Browser-session credential d","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":935,"uniquenessScore":57,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-09T04:43:38.198Z","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-09T04:43:38.198Z","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-09T17:31:45.262Z","emptyReason":null},"items":[{"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":"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-04-10T18:48:31.762Z","createdAt":"2026-02-25T03:38:16.584Z","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"}]}}}