| name | x-surfing |
| description | Drive x.com (Twitter) through pocket-browser's real-device WKWebView — follow, like, reply, post, and verify every write. Use when an agent needs to operate a logged-in X account via the pocket /pocket/cmd API instead of a headless browser. Includes hard-won pitfalls: React synthetic-click immunity, newline-swallowing composer, UserCell targeting, and verification discipline. |
X 冲浪 skill(pocket-browser 版)
用 pocket-browser 的真机 WKWebView 操作 x.com。所有工艺都在真实账号上实战验证过,所有"⚠️"都是翻过的车。
前提:手机端 WebView 已登录目标 X 账号,/pocket/status 返回 phone_connected: true。
基本功
curl -s -X POST http://127.0.0.1:3897/pocket/cmd \
-H "Authorization: Bearer $POCKET_TOKEN" \
-H "Content-Type: application/json" -d @/tmp/cmd.json
goto 大站要 "timeout_ms": 30000 起步;返回 "loading..." 只代表开始加载,不代表加载完。goto 后等 8–10 秒再动手。
- 读页面优先
js([...document.querySelectorAll('article')].map(a=>a.textContent)),比 screenshot 快且稳;x.com/home 这类重页面截图经常直接 timeout。
- 页面状态是流沙:每次操作前确认目标元素真的存在(
articles.length > 0),不要信上一秒的 DOM。
⚠️ 四条铁律(每条都是事故换的)
- React 无视合成 click()。
btn.click() 返回正常、页面纹丝不动。必须 dispatch 完整事件链:
['pointerdown','mousedown','pointerup','mouseup','click'].forEach(t =>
btn.dispatchEvent(new MouseEvent(t, {bubbles: true, cancelable: true, view: window})));
-
发推正文一律单段、无换行。composer 里 insertText 带 \n 会吞内容——只发出最后一段,而且 sent 返回 true、字数看起来正常,全是假象。要分段感就用句号和破折号。
-
UserCell 内精确定位,永不批量点击。回关/点赞先找到目标 cell 再在 cell 内找按钮:
const cell = [...document.querySelectorAll('[data-testid=UserCell]')]
.find(c => c.textContent.includes('@TargetHandle'));
const btn = [...cell.querySelectorAll('button')].find(b => /follow back/i.test(b.textContent));
批量点击的事件冒泡会点进别人主页、误关推荐位的路人。非 ASCII 显示名(韩文、日文)在传输链路可能碎掉——匹配一律用 ASCII 的 @handle。
-
js 返回值 ≠ 实际生效。每个写操作后必须独立验证:回关后重读按钮文本是否变 "Following";发推后 goto 回主页读第一条 article 确认正文上墙。"CLICKED"/"sent" 都不算数,DOM 状态才算。
-
验证必须刷新页面(重新 goto)再读 DOM,读旧 DOM 会误判失败。详情页不会实时插入你刚发的回复——原地读 DOM 看不到 ≠ 没发出去。误判失败后盲目重试也不会堆出重复内容:X 对同文重复会提示"已发过"并拦截(天然防双发保险丝)。反过来说,重试一直"失败"时,第一次很可能已经成功了——先 goto 刷新验证,再决定要不要重试。
发推
- 字数预检:X 对中文按 2 字符计权,280 上限 ≈ 140 个中文字。
sum(2 if ord(c)>127 else 1 for c in text)
- goto 自己的主页再开 composer——在别人主页或推文详情页开 composer 会自动 @ 对方,推变成回复,进不了自己的 Posts 流。
- 开 composer:
a[href='/compose/post'] 或 [data-testid='SideNav_NewTweet_Button'](事件链点击)。
- 填正文:
ta.focus() 后 document.execCommand('insertText', false, text) 到 [data-testid='tweetTextarea_0']。单段,见铁律 2。
- 发送:
[data-testid='tweetButton'](事件链点击)。
- 验证:回主页读第一条 article,关键词在墙上才算发出去了。发完跳到限流提示页属正常现象,不影响已发内容。
回复
- 从 timeline 或对方主页点击目标推文进入详情——不要直接 goto status URL,受限账号会被重定向,回复变成独立发推。
- 点击前确认目标 article 真的找到了(find 返回非 null)。页面没加载完时 reply 按钮会命中残留 DOM,回复会错楼。
- 在目标 article 内找
[data-testid='reply']——详情页第一个 article 是主帖,不要用页面第一个 reply 按钮。
- insertText 单段正文 → tweetButton → 验证。
点赞 / 回关
- 点赞:目标 article 内
[data-testid='like'],事件链点击,读 [data-testid='unlike'] 出现验证。
- 回关:铁律 3 的 cell 内定位,点击后重读按钮文本变 "Following" 才算成功。
删推
目标 article 内 [data-testid='caret'] → menuitem 含 Delete → [data-testid='confirmationSheetConfirm']。
⚠️ 永不按文本内容匹配做删除——同文的新旧两条会误中先渲染的那条。删除前确认候选唯一,或用时间戳/位置区分。
维护本地关注名册
不要每次都开 followers/following 页现场找人——页面是虚拟列表,加载慢、滚动不可靠,还容易在匆忙中误点。在本地文件里维护一份名册:
- 记什么:@handle(ASCII,用于 cell 匹配)、显示名、互关/单向状态、一句话备注、快照日期。
- 纪律:每次回关、取关、发现新粉丝后立即更新名册,让它始终可信;写操作前先查名册再上页面,目标明确才动手。
- 单向关注单独列出并注明原因(等回关中/疑似平台抽风取关/观察截止日),避免下次盘点时又变成"不知道是谁"。
- 名册是账号的私人数据,放在你自己的工作目录,不要提交进公开仓库。
行为纪律
- timeline 是算法流,刷新即换内容。要找特定推文去对方主页找;主页虚拟列表
scrollBy 不一定触发加载,翻不到就止损,改天再来。
- 发推频率一天 2–4 条是人,再多像嗑药。
- 写操作失败先截图看现场,不要盲重试——上一次的"失败"可能已经生效了一半。
为什么用真机而不是 headless
X 对数据中心 IP 和 headless 特征降权甚至阉割 DOM。pocket-browser 的执行端是真 iPhone、真 WebKit、真移动 UA、真家宽网络,登录态就在手机里——这条链路发出的每个动作,和主人亲手点的没有区别。区别只有一个:是谁的手。