Skip to content

feat(twitter): GraphQL-based lists + list-tweets + list-add/remove#1076

Merged
jackwener merged 4 commits intojackwener:mainfrom
huanghe:feat/twitter-list-management
Apr 18, 2026
Merged

feat(twitter): GraphQL-based lists + list-tweets + list-add/remove#1076
jackwener merged 4 commits intojackwener:mainfrom
huanghe:feat/twitter-list-management

Conversation

@huanghe
Copy link
Copy Markdown
Contributor

@huanghe huanghe commented Apr 18, 2026

概要

#1053 基础上做两类增强,改进 Twitter list 处理:

  1. 用 GraphQL ListsManagementPageTimeline 重写 twitter lists(commit 1)。一次 API 调用即返回所有 owned + subscribed 列表,字段精确(member_count / subscriber_count)。去掉 PR fix(twitter): repair lists scraping from detail pages #1053 里脆弱的 DOM-scraping + detail-click 主循环(约 240 行),也省掉"N 个 list = N+1 次整页加载"的性能开销。X 改 overview 页 DOM 时这条命令不会再跟着崩。
  2. 新增 twitter list-tweets <listId>(commit 1)。读取 list 的推文流,走 bookmarks.js / likes.js 同一套 GraphQL 模板(BEARER + ct0 + 动态 queryId + cursor 分页)。读侧闭环:lists 拿 id → list-tweets 拉推文。
  3. 新增 twitter list-add / twitter list-remove(commit 2)。写操作,把用户加入 / 移出自己的 list。

commit 2 为何是这个实现

落地之前尝试了三种方案都不通:

  • GraphQL ListAddMember mutation → X 返回 BadRequest: com.twitter.strato.serialization.DecodeException。这个 mutation 的 body schema 受 features 字段约束,从 client-web bundle 里抓不到可自动化复用的完整形状。
  • v1.1 REST /1.1/lists/members/create.json → HTTP 401(需要 OAuth1.0a 签名,仅靠 bearer+ct0 不足以写 v1.1)。
  • DOM .click() 点 dialog 里的 row + 关闭 → 静默什么都没做。X 的 list dialog 是 "click 只做暂存,Save 才真正提交" 模式:点 row 仅翻转 aria-checked(乐观 UI),不会发 POST;按 ESC 或关闭 X 等于 取消只有点右上角的 Save 按钮才会提交到服务端。而且 .click()(非 trusted event)在 row 上也不管用——X 对 list 写操作做了 isTrusted=true 过滤。

最终可靠的路径:

  • page.nativeClick(CDP Input.dispatchMouseEvent)点 row —— trusted 事件,aria-checked 翻转
  • page.nativeClick 点 Save 按钮 —— 触发 X 真正提交
  • 再拉一次 ListsManagementPageTimeline 对比 member_count 前后差:只有 N → N±1 才算 success。绝不相信乐观 UI

真实场景验证:批量把 15 个用户加进同一个 list,每一条都能看到 member_count 递增,没有假 success。

附带变化

  • lists 去掉位置参数 user(GraphQL 只会返回当前登录用户的 list;旧版那种"抓别人 /lists 页"的行为其实从来都不稳定)。
  • lists 返回的 followers 改成精确整数(如 "8747"),不再是 overview 页的近似字符串("8.7K")。
  • 删除已废弃的 lists-parser.jslists.d.ts

测试

  • npx vitest run clis/twitter/lists.test.js clis/twitter/list-tweets.test.js clis/twitter/list-add.test.js clis/twitter/list-remove.test.js — 14/14 通过
  • npx tsc --noEmit — clean
  • opencli twitter lists --limit 10 -f json — 返回所有 list + id
  • opencli twitter list-tweets <id> --limit 5 -f json — 正常返回推文
  • opencli twitter list-add <id> <user> — 批量连续 15 次,每次都看到 member_count N→N+1
  • opencli twitter list-remove <id> <user>member_count 正常递减

背景

基于已合入的 #1053。X list dialog 的 "Save-to-commit" 模式文档里查不到,踩坑细节在 commit 2 的 message 里也有。

🤖 Generated with Claude Code

@huanghe
Copy link
Copy Markdown
Contributor Author

huanghe commented Apr 18, 2026

Twitter List(列表)是 OpenCLI 极其重要的功能模块。它打破了 5000 人的关注上限,允许用户精细化管理信息流。通过实现此功能,用户可以:

使用 OpenCLI 批量获取特定 List 下的推文。

配合 OpenCLI 的 AI 模块,对 List 内容进行自动化处理、摘要和过滤,极大地提升阅读效率。

huanghe and others added 2 commits April 18, 2026 10:09
The DOM-scraping / detail-click approach in PR jackwener#1053 remained fragile
against X's frequent overview-page rendering changes and slow (N+1 page
loads per list). Rewrite `twitter lists` to call
`ListsManagementPageTimeline` GraphQL directly — one request returns all
owned + subscribed lists with id/name/member_count/subscriber_count/mode.

Also add `twitter list-tweets <listId>` for pulling the tweet stream from
a list, completing the read-side chain (lists → pick an id → list-tweets).

- lists: drop positional `user` arg (GraphQL returns only logged-in
  user's lists), add `id` column, change followers to exact integer from
  subscriber_count.
- list-tweets: same GraphQL pattern as bookmarks/likes (BEARER + ct0 +
  dynamic queryId with static fallback + cursor pagination).
- Delete obsolete lists-parser.js and lists.d.ts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two new commands to toggle list membership. X's list dialog uses a
"click-to-stage, Save-to-commit" pattern — clicking a row only updates
optimistic UI; the actual POST fires when the user clicks the top-right
"Save" button. Pressing ESC or the close-X silently cancels the change.

Implementation:
- Resolve listId → name via ListsManagementPageTimeline GraphQL, so we
  match the dialog row by name (dialog rows have no data-testid listId).
- Open profile page → DOM click "…" menu → "Add/remove from Lists".
- Scroll dialog to locate target row (virtualized list).
- page.nativeClick on row — trusted CDP Input.dispatchMouseEvent fires
  React's onclick, flips aria-checked (.click() alone does not suffice;
  X ignores non-trusted events for list mutations).
- page.nativeClick on the Save button — commits to server.
- Verify by re-fetching ListsManagementPageTimeline and diffing
  member_count: success only if N→N±1. No silent successes.

This fixes the pattern where batch `list-add` calls returned success for
every user but committed zero to the server (optimistic UI lied).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@huanghe huanghe force-pushed the feat/twitter-list-management branch from 72b8521 to 673031a Compare April 18, 2026 02:10
@huanghe
Copy link
Copy Markdown
Contributor Author

huanghe commented Apr 18, 2026

Code review

Found 1 issue:

  1. cli-manifest.json entry for twitter/lists has the wrong modulePath/sourceFile, pointing at twitter/list-add.js instead of twitter/lists.js. The lists command and the list-add command live in separate files (each with its own cli({...}) registration), so this is a real inconsistency — not the intentional multi-command-in-one-file pattern used by e.g. spotify/*. The command currently still works (filesystem scan fallback appears to pick up the real lists.js), but the manifest fast-path will resolve to the wrong module.

OpenCLI/cli-manifest.json

Lines 15160 to 15166 in 673031a

"type": "js",
"modulePath": "twitter/list-add.js",
"sourceFile": "twitter/list-add.js",
"navigateBefore": "https://x.com"
},
{
"site": "twitter",

Likely cause: I regenerated the manifest via npm run build-manifest after a temp-move of list-add.js in the working tree, which confused the scanner's name→file mapping.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@jackwener jackwener merged commit bb21e7e into jackwener:main Apr 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants