Summary
用 ego-browser 在 X (Twitter) 上发推文/发帖/引用推文。当用户要求发布推文、测试发帖、引用某条推文发自己的评论、或提到 x.com 发帖流程时使用。包含实测验证过的可靠流程和已知坑位(contenteditable composer、双 composer 选择器、后台 tab rAF…
hexbee/hello-skills
用 ego-browser 在 X (Twitter) 上发推文/发帖/引用推文。当用户要求发布推文、测试发帖、引用某条推文发自己的评论、或提到 x.com 发帖流程时使用。?
npx skills add hexbee/hello-skills --skill x-tweet-ego
用 ego-browser 在 X (Twitter) 上发推文/发帖/引用推文。当用户要求发布推文、测试发帖、引用某条推文发自己的评论、或提到 x.com 发帖流程时使用。包含实测验证过的可靠流程和已知坑位(contenteditable composer、双 composer 选择器、后台 tab rAF…
Related neighbors and high-traction skills in the same topics — useful to compare before installing.
Install and operate Hermes Tweet, a Hermes Agent plugin for X/Twitter research, timeline readin…
1.4K installsThis skill should be used when the user asks to "트윗 가져와", "트윗 번역", "X 게시글 읽어줘", …
1.3K installsX/Twitter URL을 받으면 트윗 원문을 가져와서 요약-인사이트-전체 번역을 제공하는 스킬. "트윗 번역…
824 installsFetch tweet content directly from fxtwitter API. Use when given a tweet/X URL to extract the tw…
670 installsFetch tweets, replies, timelines, search results, X Lists, and X Articles from X/Twitter withou…
451 installsAdd X (Twitter) testimonials to the bklit homepage. Fetches username, avatar, and tweet text fr…
433 installsOther skills from hexbee/hello-skills · top by installs.
npx skills add hexbee/hello-skills
Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.
main
Files included with this skill beyond the listing page.
SKILL.md
16,673 B
SUMMARY.md
491 B
实测验证于 2026-08(个人账号实测发帖成功)。2026-08-17 补充环境差异修正:截图不可用兜底、引用菜单键盘导航优先、标签页替换防御。
skill://ego-browser 获取完整 helper API(useOrCreateTaskSpace、openOrReuseTab、click、typeText、pressKey、js、cdp、completeTaskSpace 等的签名与语义)。本文件不重复这些 API 定义。ego-browser nodejs <<'EOF' ... EOF heredoc 执行。skill://ego-browser 的 references/install.md 安装后再执行。一次 heredoc 完成全部步骤(打开 → 强制渲染 → 输入 → 门禁 → 发帖 → 验证 → 收尾),不要拆成多轮 heredoc 来回探查。直接套用下面的模板,只改 task space 名 和 text:
ego-browser nodejs <<'EOF'
const task = await useOrCreateTaskSpace('发推:<任务名>')
const text = '<推文正文,支持中文与 \\n 换行>'
// 通用 helper(坑位 7/10):截图失败不致命(只是强制渲染手段),DOM 轮询兜底;页面不在 X 就重进
async function ensureMounted(sel, ms = 10000) {
try { await captureScreenshot() } catch (e) { cliLog('screenshot unavailable (non-fatal): ' + e.message) }
const deadline = Date.now() + ms
while (Date.now() < deadline) {
if (await js(String.raw`!!document.querySelector(${JSON.stringify(sel)})`)) return true
await wait(1)
}
return false
}
async function ensureOnX(url) {
const u = (await pageInfo()).url
if (!u.includes('x.com')) { await gotoUrl(url); await waitForLoad({ timeout: 30 }); await wait(3) }
}
// 1. 打开已登录的发帖弹窗,确认页面真的在 X(坑位 10:标签页可能被替换成新标签页)
await openOrReuseTab('https://x.com/compose/post', { wait: true, timeout: 30 })
await ensureOnX('https://x.com/compose/post')
// 2. 强制渲染 + 等 composer 挂载(坑位 7:后台 tab rAF 冻结靠截图解冻;截图不可用时页面可见则动画正常走)
if (!(await ensureMounted('[data-testid="tweetTextarea_0"]'))) { cliLog('ABORT: composer not mounted'); await completeTaskSpace(task.id, { keep: false }); process.exit(1) }
// 3. 真实鼠标点击聚焦 + 真实按键输入(中文无问题)
await click('[data-testid="tweetTextarea_0"]', { label: 'focus composer' })
await typeText(text)
// 4. 发帖前门禁:文本完全匹配 + 按钮可用。不满足就 ABORT,绝不点发帖
const gate = await js(String.raw`(() => {
const comp = document.querySelector('[data-testid="tweetTextarea_0"]')
const dlg = comp ? comp.closest('[role="dialog"]') : null
const btn = dlg ? dlg.querySelector('[data-testid="tweetButton"]') : null
return {
ok: !!comp && !!btn && comp.innerText.trim() === ${JSON.stringify(text)} && btn.disabled === false,
innerText: comp ? comp.innerText : null,
btnDisabled: btn ? btn.disabled : null
}
})()`)
if (!gate.ok) { cliLog('ABORT: ' + JSON.stringify(gate)); await completeTaskSpace(task.id, { keep: false }); process.exit(1) }
// 5. 发帖
await click('[role="dialog"] [data-testid="tweetButton"]', { label: 'click post' })
await wait(5)
// 6. 验证成功:dialog 已关闭 + URL 回 /home + 出现成功 toast
const result = await js(String.raw`(() => ({
url: location.href,
dialogOpen: !![...document.querySelectorAll('[role="dialog"]')].find(d => d.querySelector('[data-testid="tweetTextarea_0"]')),
toast: [...document.querySelectorAll('[data-testid="toast"]')].map(t => t.innerText)
}))()`)
cliLog('posted: ' + JSON.stringify(result))
// 7. 收尾(keep:false 关闭任务空间)
await completeTaskSpace(task.id, { keep: false })
EOF
要点:
ABORT 原因即停,不需要任何中间轮次的探查。ok === false 时绝不点发帖按钮;发帖后若 dialog 未关/URL 未跳,先读 result 判断,不要重复点发帖(可能重复发帖)。https://x.com/<username> 核对时间线顶部(article 的 innerText 首段即正文);正常情况不必,省一次导航。引用入口在转帖按钮的菜单里(不是独立按钮):点 [data-testid="retweet"] → 菜单含「转帖」和「引用」两项 → 选「引用」打开引用弹窗,在弹窗里输入自己的正文。同样一次 heredoc 跑完:
ego-browser nodejs <<'EOF'
const task = await useOrCreateTaskSpace('引推:<任务名>')
const text = '<引推正文>'
const tweetUrl = 'https://x.com/<user>/status/<id>'
// 通用 helper(坑位 7/10):截图失败不致命(只是强制渲染手段),DOM 轮询兜底;页面不在 X 就重进
async function ensureMounted(sel, ms = 10000) {
try { await captureScreenshot() } catch (e) { cliLog('screenshot unavailable (non-fatal): ' + e.message) }
const deadline = Date.now() + ms
while (Date.now() < deadline) {
if (await js(String.raw`!!document.querySelector(${JSON.stringify(sel)})`)) return true
await wait(1)
}
return false
}
async function ensureOnX(url) {
const u = (await pageInfo()).url
if (!u.includes('x.com')) { await gotoUrl(url); await waitForLoad({ timeout: 30 }); await wait(3) }
}
// 1. 打开原推,确认页面真的在 X(坑位 10)
await openOrReuseTab(tweetUrl, { wait: true, timeout: 30 })
await ensureOnX(tweetUrl)
if (!(await ensureMounted('article', 15000))) { cliLog('ABORT: tweet page not loaded'); await completeTaskSpace(task.id, { keep: false }); process.exit(1) }
// 2. 主推 = 详情页第一个 article,其转帖按钮滚进视口再点(坑位 8;不要用 aria-label 里的转帖数——实时变化会过期)
await js(String.raw`(() => {
const art = document.querySelector('article')
const btn = art ? art.querySelector('[data-testid="retweet"]') : null
if (!btn) return false
btn.scrollIntoView({ block: 'center' })
return true
})()`)
await wait(1)
await click('xpath=(//article)[1]//button[@data-testid="retweet"]', { label: 'open repost menu' })
await wait(2)
// 3. 等菜单出现(「引用」span 存在)
const menuOk = await js(String.raw`!![...document.querySelectorAll('span')].find(s => s.textContent.trim() === '引用')`)
if (!menuOk) { cliLog('ABORT: quote item not in menu'); await completeTaskSpace(task.id, { keep: false }); process.exit(1) }
// 4. 键盘选「引用」优先(坑位 9):菜单打开后焦点自动落在「转帖」menuitem → ↓ 到「引用」→ Enter。
// 每步验证焦点,绝不盲按 Enter(焦点若还在「转帖」上按 Enter 会直接转帖 = 误操作)
const f1 = await js(String.raw`(() => { const a = document.activeElement; return a ? { role: a.getAttribute('role'), text: (a.textContent || '').trim() } : null })()`)
if (f1 && f1.role === 'menuitem' && f1.text.startsWith('转帖')) {
await pressKey('ArrowDown')
await wait(1)
const f2 = await js(String.raw`(() => { const a = document.activeElement; return a ? (a.textContent || '').trim() : '' })()`)
if (f2.startsWith('引用')) { await pressKey('Enter') }
}
let mounted = await ensureMounted('[role="dialog"] [data-testid="tweetTextarea_0"]', 8000)
if (!mounted) {
// 兜底:坐标点「引用」span(菜单无 menuitem/无焦点的环境),先滚进视口再取坐标
const rect = await js(String.raw`(() => {
const s = [...document.querySelectorAll('span')].find(s => s.textContent.trim() === '引用')
if (!s) return null
s.scrollIntoView({ block: 'center' })
const r = s.getBoundingClientRect()
return { x: r.x + r.width/2, y: r.y + r.height/2 }
})()`)
if (rect) { await click([rect.x, rect.y], { label: 'click quote' }) }
mounted = await ensureMounted('[role="dialog"] [data-testid="tweetTextarea_0"]', 8000)
}
if (!mounted) { cliLog('ABORT: quote dialog not mounted'); await completeTaskSpace(task.id, { keep: false }); process.exit(1) }
// 5. 聚焦引用弹窗 composer + 真实按键输入(composer/按钮 testid 与普通发帖相同,用 dialog 限定)
await click('[role="dialog"] [data-testid="tweetTextarea_0"]', { label: 'focus quote composer' })
await typeText(text)
// 6. 门禁:文本完全匹配 + 按钮可用。不满足就 ABORT,绝不点发帖
const gate = await js(String.raw`(() => {
const comp = document.querySelector('[role="dialog"] [data-testid="tweetTextarea_0"]')
const btn = document.querySelector('[role="dialog"] [data-testid="tweetButton"]')
return {
ok: !!comp && !!btn && comp.innerText.trim() === ${JSON.stringify(text)} && btn.disabled === false,
innerText: comp ? comp.innerText : null,
btnDisabled: btn ? btn.disabled : null
}
})()`)
if (!gate.ok) { cliLog('ABORT: ' + JSON.stringify(gate)); await completeTaskSpace(task.id, { keep: false }); process.exit(1) }
// 7. 发帖 + 验证:引用成功后 URL 停在原推页面(不回 /home),以 dialog 关闭 + toast 为准
await click('[role="dialog"] [data-testid="tweetButton"]', { label: 'click post' })
await wait(6)
const result = await js(String.raw`(() => ({
dialogOpen: !![...document.querySelectorAll('[role="dialog"]')].find(d => d.querySelector('[data-testid="tweetTextarea_0"]')),
toast: [...document.querySelectorAll('[data-testid="toast"]')].map(t => t.innerText)
}))()`)
cliLog('posted: ' + JSON.stringify(result))
await completeTaskSpace(task.id, { keep: false })
EOF
fillInput 会把文本写进 DOM,但不触发 React onChange → 发帖按钮保持禁用。按钮禁用 = 没写进去。click 聚焦 + typeText(真实按键事件)。.innerText;.value 永远是空串,别被误导。[role="dialog"] [data-testid="tweetButton"](唯一正确的按钮)[data-testid="tweetButtonInline"](空内容时禁用,不是目标)tweetTextarea_0;用 closest('[role="dialog"]') 限定。DOM 中第一个是弹窗的。document.execCommand('selectAll' / 'delete') 清空Meta+a + Backspace),或 Escape 关掉重开 dialog。element.click()(js 内直接调)X 可能不响应。一律用 helper click(selector, { label }) 真实鼠标点击。navigator.clipboard.writeTextRuntime.evaluate 超时。Input.insertText 也不可靠typeText 是唯一验证可行的输入路径。openOrReuseTab('https://x.com/compose/post';) 后 URL 停在 /compose/post,但页面上没有弹窗、没有 composer、没有 tweetButton;等待/轮询再久也不会出现。Escape,URL 会跳回 /x.com/home,说明 dialog 已挂载只是不可见。document.visibilityState === 'hidden'),浏览器冻结 requestAnimationFrame,X 的 modal 打开动画(0.15s 淡入+缩放)永远卡在首帧:opacity: 0、scale(0.92)、animation-play-state: running 不前进。dialog 内容(composer/按钮)要等动画完成才挂载到 DOM。await captureScreenshot()——截图强制合成一帧,动画瞬间走完,composer 与 tweetButton 随即挂载。之后一切正常。Page.bringToFront(隔离窗口本身隐藏,visibility 不变)、长等待、轮询、reload。只有强制合成帧的操作(截图)能解。Page.captureScreenshot 会 CDP 超时(截图完全不可用)。此时:- 若 document.visibilityState === 'visible':动画正常走,截图不是必需——模板 ensureMounted 已用 DOM 轮询兜底,截图失败只告警不中断。 - 若页面 hidden 且截图不可用:rAF 无法解冻,ensureMounted 会超时 → ABORT;改换真实 tab / 重开窗口重试。
getBoundingClientRect() 的 y 远超视口高度(实测转帖按钮 y≈1196 vs 视口高 805)。此时无论 helper 坐标点击还是选择器点击都不生效——菜单/弹窗不出现,且没有任何报错,容易误判成「X 行为变了」。scrollIntoView({ block: 'center' }) 把目标滚进视口,再点。[data-testid="retweet"](主推 + 相关推各一个),锁定主推用 (//article)[1]//button[@data-testid="retweet"](详情页第一个 article 即主推)。不要用 aria-label 里的转帖数——转帖数实时变化,feed/截图里的数字会过期(2026-08-17 实测:129 在几分钟内继续增长)。[data-testid="retweet"] 后弹出菜单含「转帖」和「引用」两项,「引用」打开的是带原文嵌入的普通 composer(testid 仍是 tweetTextarea_0 / tweetButton,用 [role="dialog"] 限定即可,见上方 Quote 流程)。role="menuitem",与早期版本「裸 span 无 menuitem」不同),↓ 到「引用」再 Enter 即打开引用弹窗。按 Enter 前必须验证焦点在「引用」——焦点若还在「转帖」上按 Enter 会直接转帖(误操作)。模板第 4 步已内置逐步焦点验证。scrollIntoView({ block: 'center' }) 后取中心坐标真实鼠标点击(坑位 4:不要用 element.click())。/home),验证以 dialog 关闭 + toast「你的帖子已发送」为准。chrome://new-tab-page/,页面内容全丢,后续选择器/坐标全部落空且无任何报错。pageInfo() 校验 url 含目标域名(模板 ensureOnX 已内置),不匹配就 gotoUrl(url) + waitForLoad({ timeout: 30 }) 重进,再继续。chrome-error:// 时,用 gotoUrl(url) + waitForLoad 重进,而不是 gotoAndWait。completeTaskSpace(id, { keep: false }) 收尾。