返回全部作品

Twitter User Info

刷 Feed 时直接看到这个人是谁

浏览器扩展

刷 X 的时间线时,直接在推文旁边看到这个人是谁——简介、粉丝数、注册时间,以及你自己给他写的备注和标签。

X 原生的悬浮卡只有简介,而且不能加任何属于你的信息。这个扩展补的就是那一块。

它做什么

  • 悬停出卡:鼠标停在头像或用户名上弹出完整档案。延迟可调,也可以改成按住 Alt 才出、或者只在点击时出。默认会把 X 自己那张悬浮卡关掉——内容重复,而且会跟这张叠在一起。
  • 私人备注:给任何人写一句话(「搞 RAG 的」「面基过」「只看他技术推」),直接显示在 Feed 的用户名旁边。微信式的备注名,在 X 上终于有了。
  • 标签:一个人可以打多个。逛自己某个列表的成员页时,成员会自动被打上以列表名命名的标签——把 X 那个半残的列表功能变成能用的标签系统。
  • 导出导入:备注和标签是纯本地数据,一键导出 JSON,换设备导回去。

数据全部留在本地,不往任何服务器发东西。

数据是怎么来的

不额外发任何请求。 X 加载时间线时走的是自己的内部 GraphQL 接口,响应 JSON 里每条推文本来就带着作者的完整档案——简介、粉丝数、注册时间全都有。扩展做的只是把这份已经到了你浏览器里的数据截下来。

其他几条路都不通:主动调 X 的内部 API 有速率限制、高频会触发风控;官方 API v2 免费额度极低、收费贵得离谱;从 DOM 上刮的话,Feed 的 DOM 里根本没有简介,刮不到。

不打扰页面上的其他脚本

这是这个项目的硬约束。推特生态里有大量用户脚本——媒体下载、自动翻译、时间线增强——它们的原理跟这个扩展一模一样:hook fetch / XHR 去截 GraphQL 响应。也就是说必然跟它们叠在同一个 prototype 上,而且加载顺序不可控。同时它们还会按 DOM 结构定位推文元素。任何一处不克制的实现,都会让别人的脚本莫名其妙地坏掉,而用户只会觉得是这个扩展的锅。

所以:

  • 一个 DOM 节点都不加。备注用 data-tui-note 属性加 CSS ::after 画,伪元素不进 DOM 树。
  • 不截事件。全程没有 preventDefault / stopPropagation,悬停触发不需要碰事件流。
  • 不污染 <head>。样式走 Constructed Stylesheet(adoptedStyleSheets),不插 <style> 节点;也不给 <html> 加 class 或属性,开关靠重写样式表内容实现。
  • 不常驻。悬浮卡宿主只在显示时挂载,关掉立刻摘除;keydown / pointerdown 只在卡片打开期间注册。
  • 不广播消息。跨 world 通信用带命名空间的 CustomEvent,不用 postMessage——监听 message 的脚本会全部收到。
  • 网络钩子透明。Reflect.apply 原样转发,保留 name / length / toString(),非 GraphQL 请求零介入,URL 存 WeakMap 而不是挂在实例上。

最后剩下的可见痕迹只有两处,都是功能必需:X 自己元素上的两个 data-tui-* 属性,以及悬停瞬间存在的那一个 Shadow DOM 宿主。

图标的一条硬约束

全平涂,不许出现渐变和投影。 图标要在 16px 下渲染,渐变会糊成脏色块,投影会变成一层灰边。

白底那圈描边不是装饰而是必要的:卡片本身是纯白,而 Chrome 亮色工具栏是 #F1F3F4,没有描边时卡的边缘会整个消失,只剩中间几个色块浮着。描边是纯色平涂,不违反上面那条。

安装

还没上 Chrome 应用店(需要 5 美元开发者注册费),所以走「加载已解压的扩展程序」。Chrome 同时禁止安装非应用店来源的 .crx,这是唯一的途径。

更新时下载新包覆盖原文件夹再刷新即可,备注不会丢——扩展 ID 是钉死的,跟文件夹路径无关。

技术

WXT + TypeScript,Chrome MV3,Firefox 也能构建。备注、标签、设置走 storage.local(量小、popup 能直接读写、一次拉进内存后渲染 Feed 时可以同步查);档案缓存走 IndexedDB。

Twitter User Info — 1
Twitter User Info — 2
上线
2026
我的角色
独立开发
技术栈
WXTTypeScriptChrome MV3IndexedDB