Files
jakevin 4ef2cb8b1c enrich(toutiao): hot board (public) + bug fixes (silent column drop, partial render) (#1366)
* enrich(toutiao): hot board + bug fixes (silent column drop, partial render)

Per WAWQAQ "丰富现有 adapter" pivot — Phase 2 site #3.

## New command
- `toutiao hot` (Strategy.PUBLIC, browser:false) — public homepage hot
  board via the toutiao.com hot-event/hot-board endpoint. No login required.
  Returns 8 stable columns (rank/id/title/query/hot_value/label/url/image).

## Bug fixes for `toutiao articles`
- **Silent column drop fixed**: `parseToutiaoArticlesText` previously
  did `if (title && stats) push(...)`, silently dropping any row where
  the stats span hadn't finished rendering by the time page.innerText
  was read. Slow-render bugs were invisible — adapter looked "complete"
  while writers saw extra rows in the dashboard. Partial rows now
  surface with `null` stat columns.
- **Silent clamp on `--page` removed**: out-of-range / non-integer
  values raise `ArgumentError` with explicit bounds [1, 4]. Same
  validation reused by both `articles` and `hot` via `parseArticlesPage`
  / `parseHotLimit` in `utils.js`.
- **Empty result typed**: zero-row scrape now raises `EmptyResultError`
  instead of returning `[]` silently (would otherwise look like a
  legitimate "no articles" response).

## Refactor
- Parser logic extracted to `clis/toutiao/utils.js` (alongside hot-row
  mapping, validators, and the hot-board URL constant).
- `articles.js` switches from declarative `pipeline:` to imperative
  `func` form so `parseArticlesPage` validation can run before the
  navigation step (declarative pipeline can't pre-validate args).
- Strategy is now explicit: `Strategy.COOKIE, browser: true` for
  articles (creator dashboard is logged-in only).

## hot field map
`ClusterIdStr` (or numeric `ClusterId`) → id; `Title` → title;
`QueryWord` → query (falls back to title); `HotValue` → hot_value
(non-negative numeric, else null); `Label`, `Url`, `Image` →
respective columns. `pickImage` walks `Image.url` → first truthy
`Image.url_list[]`. Empty-title rows are dropped (returns null) before
ranks are densely re-assigned 1..N.

## Tests
29 contract assertions across `parseArticlesPage` / `parseHotLimit` /
`parseToutiaoArticlesText` / `mapHotRow` + registry-level shape checks
+ `hot` adapter func behaviour (typed errors / no silent clamp / fetch
failure paths / dense-rank).

## Audits
- typed-error-lint: 196 = 196 (unchanged baseline)
- silent-column-drop: 103 = 103 (unchanged baseline)
- listing-id-pairing: hot has `id` column (round-trippable when a
  detail command lands later); advisory list unchanged.

## Manifest
757 → 758 entries (+1 for `hot`).

## Doc
- index.md: toutiao mode 🔐 → 🌐/🔐 (hot is public, articles is logged-in)
- toutiao.md: per-command mode/domain table + column docs + prerequisites

* fix(toutiao): tighten hot and articles contracts
2026-05-06 23:17:32 +08:00

2.3 KiB

Toutiao (今日头条)

Mode: 🌐 / 🔐

Command Mode Domain Description
opencli toutiao articles 🔐 Browser mp.toutiao.com 头条号创作者后台文章列表及数据(需登录)
opencli toutiao hot 🌐 Public www.toutiao.com 今日头条首页热榜(公开,无需登录)

Usage Examples

# Public hot board (no login)
opencli toutiao hot
opencli toutiao hot --limit 10
opencli toutiao hot -f json

# Creator dashboard articles (logged-in)
opencli toutiao articles
opencli toutiao articles --page 2
opencli toutiao articles --page 1 -f json

Output

hot

Column Type Notes
rank int 1-based, dense after empty-title rows are dropped
group_id string | null Topic/cluster id (ClusterIdStr, falls back to numeric ClusterId)
title string Trending topic title
query string Search keyword (QueryWord); falls back to title if absent
hot_value int | null HotValue parsed as non-negative number; null if missing
label string | null Hot tag (e.g. 热 / 新 / 沸) when present
url string | null Topic permalink
image_url string | null First non-empty image URL (Image.url → Image.url_list[*])

articles

title · date · status · 展现 · 阅读 · 点赞 · 评论

If a row's stats span has not finished rendering by the time the page text is read, the row still surfaces with null stat columns instead of being silently dropped — this masking previously hid creator-backend slow-render bugs. Login/captcha pages abort with AuthRequiredError; browser render failures abort with CommandExecutionError.

Prerequisites

hot

  • No login required. Uses the public hot-event/hot-board endpoint that powers the homepage hot panel.

articles

Notes

  • hot --limit accepts integers in [1, 50]; out-of-range values raise an ArgumentError (no silent clamp).
  • articles --page accepts integers in [1, 4], matching the contributor's verified range on the creator dashboard; out-of-range values raise an ArgumentError.