关于 xsearch
xsearch 是通过 browser-harness-js 搜索 X 并提取结构化帖子结果的命令行技能,适合浏览器中已登录 X 的用户。支持格式化或 JSON 输出,也可并行搜索。
主要功能
- 01搜索 X 中的匹配帖子
- 02提取作者、账号、文本、链接和时间
- 03输出格式化结果或 JSON
- 04并行运行多项搜索
- 05通过 browser-harness-js 打开帖子链接
适用场景
- 01收集符合查询条件的近期帖子
- 02获取推文详情供后续处理
- 03并发搜索多个主题
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ monotykamary/ browser-harness-js --skill xsearch安装后在 Claude Code 中输入/xsearch确认已加载。
附带文件scripts/setupscripts/xsearch
详细介绍
X 搜索
⚠️ 你必须在浏览器中登录 X。 X 的搜索页面不会向未登录的访客显示结果——而是会将其重定向到登录页面。
browser-harness-js使用的浏览器会话必须处于已登录 X 的状态。
通过 CDP 搜索 X(Twitter)并提取结构化结果。除提供 CDP 会话的 browser-harness-js 外,无需其他外部依赖。每次调用都会打开自己的标签页和 WebSocket 会话,可安全并行使用。
快速搜索
xsearch "your query" # pretty-printed, up to 10 results
xsearch "your query" 5 # 5 results, pretty-printed
xsearch --json "your query" 3 # raw JSON
并行使用
每次 xsearch 调用都会复用共享 WebSocket,但会通过专属的 sessionId 连接到自己的标签页。标签页通过 Target.closeTarget 以即发即忘的方式关闭,因此调用方无需等待清理完成。
xsearch "rust async" 3 &
xsearch "go channels" 3 &
wait
结果格式
每条结果的格式为 { author, handle, text, url, time }:
[
{
"author": "Zane Chee",
"handle": "@injaneity",
"text": "people of pi, i'm excited to finally introduce browser use in pi-computer-use!",
"url": "https://x.com/injaneity/status/2065110712511500620",
"time": "2026-06-11T16:35:36.000Z"
}
]
通过 URL 查看 X 帖子
直接通过 browser-harness-js 打开结果 url(或任何 x.com/<handle>/status/<id> 永久链接)——无需重新搜索。流程与临时搜索相同:连接、创建标签页并执行评估;复用 [data-testid="tweet"] 选择器,但只获取焦点帖子(在永久链接页面上,它是 DOM 顺序中的第一个):
browser-harness-js <<'EOF'
if (!session.isConnected()) {
try { await session.connect() } catch (e) { throw new Error("Cannot connect: " + e.message) }
}
const url = "https://x.com/injaneity/status/2065110712511500620"
const t = await session.Target.createTarget({ url: "about:blank", background: true })
const { sessionId } = await session.Target.attachToTarget({ targetId: t.targetId, flatten: true })
try {
await cdp(sessionId, "Page.enable", {})
// Required — without this Chrome emits zero Page.lifecycleEvent, so networkIdle
// would never fire.
await cdp(sessionId, "Page.setLifecycleEventsEnabled", { enabled: true })
// Arm the wait BEFORE Page.navigate: lifecycle events fire once, and a fast
// load can fire networkIdle between navigate returning and the listener subscribing.
const ready = session.waitFor({ method: 'Page.lifecycleEvent', sessionId, predicate: (p) => p.name === 'networkIdle', timeoutMs: 30_000 })
await cdp(sessionId, "Page.navigate", { url })
await ready
await new Promise(r => setTimeout(r, 4000)) // React hydration; see Traps
const result = await cdp(sessionId, "Runtime.evaluate", {
expression: `(() => {
const el = document.querySelector('[data-testid="tweet"]')
if (!el) return JSON.stringify(null)
const textEl = el.querySelector('[data-testid="tweetText"]')
const timeEl = el.querySelector('time')
const userNamesEl = el.querySelector('[data-testid="User-Name"]')
const nameLinks = userNamesEl ? [...userNamesEl.querySelectorAll('a[role="link"]')] : []
return JSON.stringify({
author: nameLinks[0]?.textContent?.trim() || "",
handle: nameLinks[1]?.textContent?.trim() || "",
text: textEl?.innerText?.trim() || "",
time: timeEl?.getAttribute('datetime') || ""
})
})()`,
returnByValue: true
})
return result.result.value
} finally {
session.closeTab(t.targetId, sessionId).catch(() => {})
}
EOF
- 焦点帖子(即永久链接指向的帖子)是 DOM 顺序中第一个
[data-testid="tweet"];回复和引用的帖子会显示在它下方。 - 4s 的水合等待时间与
xsearch相同——networkIdle会在 React 渲染帖子之前触发。未登录的访客访问永久链接时也会遇到登录页面,与搜索时相同。 - 等待策略。 示例会等待
networkIdle(网络请求静默 500ms)——这是 X 页面和大多数内容网站的合适默认选项:它会在load之后触发,因此返回的内容至少一样多;而且不会像loadEventFired那样被一直挂起的广告/分析信标阻塞。针对特定页面类型的替代方案:networkAlmostIdle(静默 250ms)——适用于持续进行 XHR 轮询、无法达到完整 500ms 静默的页面。loadEventFired——确实需要加载所有子资源时使用(文本提取很少需要)。- 就绪后短暂等待:在 evaluate 之前执行
await new Promise(r => setTimeout(r, 1000))——适用于在networkIdle之后才延迟渲染内容的页面。
工作原理
| 步骤 | CDP 调用 | 功能 |
|---|---|---|
| 1 | session.connect()(一次) | 将共享 WebSocket 连接到浏览器 |
| 2 | Target.createTarget({ background: true }) | 创建一个隔离的后台标签页 |
| 3 | Target.attachToTarget | 获取用于标签页路由的专属 sessionId |
| 4 | cdp(sessionId, "Page.enable", …) | 订阅页面事件 |
| 5 | cdp(sessionId, "Page.setLifecycleEventsEnabled", …) | 启用生命周期事件——若不启用,networkIdle 不会触发 |
| 6 | session.waitFor('Page.lifecycleEvent' networkIdle) 在 cdp(sessionId, "Page.navigate", …) 之前启动 | 修复竞态:在导航前启动 networkIdle 等待(避免页面已加载的竞态),然后前往 x.com/search?q=…&src=typed_query&f=top |
| 7 | setTimeout(4000) | 等待 React 水合并渲染帖子 |
| 8 | 滚动循环(如果 count > 6) | 滚动以加载更多帖子(每次约 3 条) |
| 9 | cdp(sessionId, "Runtime.evaluate", …) | 通过 data-testid 选择器进行单次 DOM 查询 |
| 10 | closeTab(即发即忘) | 拆除标签页,不阻塞响应 |
使用的 DOM 选择器
X 的搜索页面使用稳定的 data-testid 属性:
| 选择器 | 用途 |
|---|---|
[data-testid="tweet"] | 帖子容器 |
[data-testid="tweetText"] | 帖子正文 |
[data-testid="User-Name"] | 作者名称/账号名区块 |
a[role="link"],位于 User-Name 内 | 链接:[0]=名称,[1]=账号名,[2]=永久链接+时间戳 |
time | 通过 datetime 属性获取 ISO 时间戳 |
注意事项
- 必须已登录。 X 不会向未登录的访客显示搜索结果,而是将其重定向到登录提示页面。浏览器会话必须处于已登录 X 的状态。
- React 水合延迟。
networkIdle会在 React 渲染推文之前触发。收到就绪信号后,必须等待 4s,初始批次(约 6 条推文)才会显示。 - 滚动以加载更多结果。 X 使用无限滚动。初始视图包含约 6 条推文。当
count > 6时,脚本会向下滚动(每次滚动加载约 3 条推文,并延迟 1.5s)。 - 每个新标签页都必须调用
Page.enable()和Page.setLifecycleEventsEnabled({ enabled: true })。 后者是 Chrome 触发任何Page.lifecycleEvent的必要条件;如果不调用,networkIdle等待每次都会超时。 networkIdle等待的超时时间为 30s — 使用session.waitFor()而不是原始 promise,因此页面挂起时不会导致标签页泄漏。X 会持续加载,但实际使用中,初始网络请求通常会在约 2–3s 内平静下来;如果页面始终无法达到 500ms 的静默窗口,请改用networkAlmostIdle(参见上文的等待策略说明)。- 结果数量可能少于请求数量 — X 可能没有足够的匹配推文,或者滚动循环未能及时加载这些推文。
- 推文文本可能被截断 — 对于较长的推文,X 会显示“显示更多”按钮。提取的
text是点击“显示更多”之前可见的文本。 - 不依赖
jq— URI 编码在 JS 中使用encodeURIComponent(),输出格式则通过 heredoc 中的.map().join()完成。
评论
支持 Markdown