Skip to content

feat(space): add X Space card with availability states - #20

Merged
Chilfish merged 7 commits into
mainfrom
feat/space-card
Sep 16, 2026
Merged

Chilfish merged 7 commits into
mainfrom
feat/space-card

Conversation

@Chilfish

@Chilfish Chilfish commented Sep 16, 2026 •

Copy link
Copy Markdown
Owner

改了什么

接入 X Space(语音直播 / 录音回放)卡片:Space 推文过去在站内只剩一个裸 t.co 链接,
现在按官方卡片渲染标题 / 主播 / 收听人数 / 时长,并区分可播放状态。

上游数据链:这类推文的 card(3691233323:audiospace)是空壳,binding_values
只有 tweet_id / id / card_url,既无标题也无图片,被 mapTwitterCard() 的最终校验整个
丢弃(改动前 /api/tweet/get 实测无 card)。卡片要的字段全在另一个 GraphQL 端点
AudioSpaceById(queryId HPEisOmj1epUNLCWTYhUWw),故新增一次独立上游请求。

可播放状态机(availability,由 state + is_space_available_for_replay 推导):

availability 条件 卡片表现
replayable 已结束 + 回放已开启 「播放录音」(唯一给播放入口的分支)
live / upcoming Running / NotStarted 「直播中」/「尚未开始」
no-replay 已结束但主办方未开启回放 「录音不可回放」,不给播放入口
unavailable 上游无 metadata(已删除/不可访问) 中性墓碑条「Space 已删除或不可访问」+「在 X 查看」

Space id 双通道识别:多数推文带 audiospace 卡片,但也实测到完全没有 card、只在正文里
贴了 x.com/i/spaces/… 的推文(1871586443388420240)——只认 card binding 会把这类整条漏掉,
故 resolveSpaceId(card, entities) 优先卡片、回退正文链接。

旧缓存回填:space 是后加字段,而命中缓存时不会再走 getEnrichedTweet,旧缓存推文永远
长不出卡片(实测 cache.get … hit:true 仍然是裸链接)。故在唯一读出口 getLocalTweet() 做一次
回填/升级,best-effort 写回 localCache + DB。

范围裁定(owner 2026-09-16):只做卡片 + 跳转 X 播放,不做站内播放。录音是 HLS,m3u8 响应头
为 Access-Control-Allow-Origin: https://x.com(浏览器跨源取流被拦),站内播放需要服务端媒体
代理 + hls.js 依赖 + 相应带宽,成本远超卡片本身。

为什么

owner 直接需求:tmp/録音を再生….html(官方卡片 DOM)+ 推文样本。首轮落地后 owner 复测指出
两个边界缺失 —— 已删除的 Space / 未开启回放「没有对应的 UI 提示」,并给出两个真实样本推文
(2057046686871232598、1871586443388420240),本 PR 已全部覆盖。

无关联 issue(直连需求)。

检查项

  • bun run typecheck、bun run lint、bun run build 通过
  • bun run verify/index.ts --exit-on-fail 通过(449 passed / 7 skipped)
  • 新行为有对应测试和验收标准(verify/acceptance-criteria/AC-space.md,AC-SPACE-001~011)
  • 提交信息符合 Conventional Commits
  • 文档已同步(docs/features/tweet/space-card.md、开发日志、llms.ts + 随附 OpenAPI 快照)

验证方式

  • 门禁:pre-push 全绿 —— typecheck / eslint / verify --exit-on-fail / build /
    build-storybook 五项

  • CI:verify + Vercel 均 pass

  • 单测 / 渲染测试:AC-SPACE 11 条共 32 个用例,全离线(上游响应固化为 fixture)

  • 真实上游 E2E(getEnrichedTweet + getLocalTweet)复核三条推文:

    推文 场景 结果
    1968314084207788302 已结束 + 可回放 replayable,2,478 人 / 42:40 / 9月17日
    2057046686871232598 已删除(带卡片) unavailable 墓碑
    1871586443388420240 已删除 + 无卡片 unavailable 墓碑
  • Storybook:新增 Tweet/SpaceCard 五态(可回放 / 直播中 / 不可回放 / 已删除 / 超长标题 +
    大数字 / 窄屏)

  • 回归:旧缓存缺 availability 曾导致渲染崩溃
    (Cannot read properties of undefined (reading 'aria')),已由 resolveSpacePlaybackState()
    兜底 + 回归测试覆盖

审阅提示

  • 最后一个 commit chore(taste): … 是学习系统自动写入 .commandcode/taste/ 的偏好记录
    (改动前工作区就已 dirty)。顺带说明:该文件里那行残句
    ( rouping demoted to sub-headings)…)main 上就已存在,不是本 PR 引入的;
    本 commit 只是追加 4 条新偏好 + 修订 1 条旧偏好。若不想让 taste 记录跟着进 main,
    单独 drop 这个 commit 即可(其余 6 个 commit 不依赖它)。
  • app/lib/rettiwt-api/collections/*、enums/* 在 ESLint ignore 列表内(pre-commit 会打
    "File ignored" warning),是仓库既有配置,非本 PR 引入。

Chilfish and others added 7 commits September 17, 2026 01:42
Space 推文的 card 是空壳(binding 只有 tweet_id / id / card_url,无
title/description/图片),标题/主播/人数/时长全在另一个 GraphQL 端点里,
故新增资源与请求构造:

- ResourceType.SPACE_DETAILS + SpaceRequests.details(id)
  (queryId HPEisOmj1epUNLCWTYhUWw;withReplays: true 才能拿到已结束场次)
- Requests / FetchResourcesGroup / index.ts 三处注册
  (漏注册会被 _validateArgs 静默判废)
- 补 data.audioSpace 响应壳类型(metadata 结构对齐既有 Details.ts)

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
按 Postmortem #2(纯逻辑必须脱离 React 才能验证)把所有判定下沉为
无 IO 纯函数,组件只做渲染:

- SpaceDetails / SpaceHost / SpaceAvailability 类型,EnrichedTweet.space 可选
- extractAudiospaceId:从 card binding 取 Space id
- extractSpaceIdFromUrl / resolveSpaceId:无卡片推文回退正文链接解析
- mapSpaceDetails:metadata → 卡片字段(标题/主播/人数/时长/可播放状态)
- resolveSpacePlaybackState:availability 兜底(兼容缺该字段的旧缓存)
- formatSpaceDuration / formatSpaceDate / formatSpaceListeners

字段口径对齐官方卡片实测值:listenersCount = 直播收听 + 回放(2,478)、
durationMs = ended_at - started_at(42:40)、created_at → 9月17日。

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
取数与降级(不触碰 parseTweet.ts —— Postmortem #1 的头号高危文件,
Space 识别放在 enrichTweet 之外的异步层读原始 card):

- fetchSpaceDetails / resolveSpaceById:按 id 取 AudioSpaceById 并映射
- attachSpaceDetails:仅单推文路径挂 space;请求失败不挂(否则会把限流
  误报成「已删除」),仅记 obsLog('space.fetch.failed')
- getLocalTweet 出口回填:space 是后加字段,命中缓存的旧推文不会再走
  getEnrichedTweet,于是卡片永远不出现(实测线上仍是裸链接)。缓存不保留
  card 但链接始终在实体里,故按 resolveSpaceId(null, entities) 回填/升级,
  best-effort 写回 localCache + DB,避免每次浏览重打上游

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
对标官方渲染 DOM(紫底白字、主播行、标题、收听/回放 · 日期 · 时长、
行动按钮),并覆盖边界态:

- replayable → 播放录音(唯一给播放入口的分支)
- no-replay → 录音不可回放(没录音却摆播放入口 = 把人送进播不了的页面)
- live / upcoming → 直播中 / 尚未开始
- unavailable(上游无 metadata,已删除/不可访问)→ 中性墓碑条
  「Space 已删除或不可访问」+「在 X 查看」,不再只剩一个裸链接

整卡即指向 Space 页面的链接(本期不做站内播放:m3u8 只给 x.com 放行
CORS),故行动区是视觉元素而非嵌套交互元素。挂载于 TweetNode /
PlainTweet;正文里同一条 Space 链接不再重复渲染。VerifiedBadge 的 user
入参放宽为认证三字段(安全放宽,Space 主播可复用)。

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
全离线可跑:上游响应固化为 fixture(真实 TweetResultByRestId /
AudioSpaceById 响应裁剪),无需 TWEET_KEYS 或服务器。

- AC-SPACE-001~004/008/009/010 纯函数单测(含旧缓存缺 availability 的崩溃
  回归:Cannot read properties of undefined (reading 'aria'))
- AC-SPACE-005~007/009/010 渲染测试(renderToString + HTML 断言)
- AC-SPACE-011 服务层回填契约(mock 缓存层与上游取数)
- fixture:Space 卡片 binding、Ended 元数据、无 metadata 的已删除样本、
  无卡片的推文样本,以及由线上 enrichTweet 派生的完整 EnrichedTweet
- Storybook Tweet/SpaceCard 五态(可回放 / 直播中 / 不可回放 / 已删除 /
  超长标题)
- verify CLI 注册 `--module space`(AC-SPACE 前缀)

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- 新增功能文档 docs/features/tweet/space-card.md:问题(空壳 card)、
  数据链、边界态状态机、范围裁定(只做卡片 + 跳转 X 播放)、实现落点、
  已知限制
- 开发日志 2026-09-16:含边界态补全与旧缓存回填两轮返工的踩坑
- backlog 归档记录 + development-log 索引
- llms.ts 的 EnrichedTweet 补 space 字段(Data Shapes 与 OpenAPI schema),
  随附 skill 快照重新生成(diff 仅 +4 行,无version 漂移)

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
@vercel

vercel Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
anon-tweet Ready Ready Preview Sep 16, 2026 5:45pm UTC

@Chilfish
Chilfish merged commit 6aeb6c4 into main Sep 16, 2026
2 checks passed
@Chilfish
Chilfish deleted the feat/space-card branch September 16, 2026 17:48

This branch was successfully deployed

1 active deployment
Preview — 748f71c8 Deployed Sep 16, 2026 by vercel[bot]
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.

1 participant