- scan.acfun.cn QR flow (start -> long-poll scanResult -> acceptResult) with rotated-signature handling, background poller, durable session in the Host credential service, and cookie handoff to the provider - Client half: Settings -> AcFun page (status, QR code, poll, logout) over a same-origin /acfun-panel JSON API with cross-origin rejection - acfun_moment / acfun_moment_comments / acfun_topic read tools, moment_like / moment_unlike / moment_banana actions, and comment posting on moments - acfun_account status tool; docs for the QR contract, the 广场/动态 research verdict, and the client-composition restart requirement
601 lines
37 KiB
Markdown
601 lines
37 KiB
Markdown
# AcFun 广场 / 动态 (moment feed) — reverse-engineered for a Node.js `fetch` plugin
|
||
|
||
All probes were executed live on **2026-10-27 (Asia/Shanghai)** against `acfun.cn` hosts
|
||
(`www.acfun.cn`, `m.acfun.cn`, `api-ipv6.acfunchina.com`, `kuaishouzt.com`) using a desktop Chrome
|
||
User-Agent and `Referer: https://www.acfun.cn/` (or `https://m.acfun.cn/` for mobile hosts).
|
||
No AcFun account was used. Every claim below is marked **[VERIFIED]** (called live, real
|
||
status/JSON/payload pasted) or **[UNVERIFIED]** (contract read out of the site's own JS, or a write
|
||
that could only be probed up to its auth error). **No write was ever executed.**
|
||
|
||
Probe scripts live in `%TEMP%\acfun-moment-probe\` (`p1…p33*.mjs`) and are not part of the repo.
|
||
|
||
---
|
||
|
||
## 0. Verdict first (read this if nothing else)
|
||
|
||
| Question | Answer |
|
||
|---|---|
|
||
| Is there an official **web** 广场/动态 feed? | **No.** `https://www.acfun.cn/moment/` → `302 → /404`. The www nav contains no 动态/广场 entry at all. `/member/feeds` exists but `302`s anonymous visitors to the login page. There is **no moment bundle** in the www app (40 bundles / 50 `/rest/` paths scanned; only one internal bundle mentions `moment`). |
|
||
| Does a **usable moment read API** exist? | **Yes — one.** `GET /rest/pc-direct/moment/detail?momentId={id}` returns a full moment object **anonymously**. Moment comments work anonymously too (`comment/listByFloor` with `sourceType=4`). |
|
||
| Is there a public "square"/recommend **list** API? | **Not found.** ~560 guessed paths across `/rest/pc-direct/*`, `/rest/mobile-direct/*`, `/rest/app/*`, `/rest/zt/*` and five hosts. The only moment *lists* are (a) **login-scoped** following feeds (`feed/followFeedV2`, `feed/followDougaFeed`, `feed/webPush`) and (b) the **mobile-web topic page HTML**, which server-renders a fixed 15 items with no pagination. |
|
||
| Can a plugin post a 动态? | **No.** Publish is app-only: `POST https://api-ipv6.acfunchina.com/rest/app/moment/add` → `401 {"result":-401,"error_msg":"token value error"}`. Requires the signed `acfun.midground.api_st` token *(see §5)*, i.e. an AcFun **app** session, not a web cookie. |
|
||
| What is the real 2026 surface? | **社区圈 / 话题 (`communityCircle`)** on mobile web: `https://m.acfun.cn/communityCircle/{tagId}` is alive with freshly-dated content (observed `2026-10-13`), a topic index (e.g. 教育 = `38关注 | 10458内容`), and moment ids in a **new 7-digit space** (e.g. `5093831`). |
|
||
| Does the community build its own web 广场? | Yes — "[【油猴脚本】网页端动态广场!](https://www.acfun.cn/a/ac48817170)" (2026-09-01) plus Greasy Fork scripts `579890` / `584032`. That is direct evidence there is an API but **no official web UI**. (Greasy Fork was unreachable from this network, so their exact endpoint list could not be read; the endpoints below were derived from AcFun's own bundles and verified live.) |
|
||
|
||
**Bottom line:** a plugin can be a **moment reader + comment reader** today, with no cookie at all,
|
||
plus a cookie-gated "following feed" reader. Every write except maybe like/banana/comment is
|
||
effectively impossible.
|
||
|
||
---
|
||
|
||
## 1. Hosts and URL prefixes (the one thing that trips everyone up)
|
||
|
||
| Purpose | Host / prefix |
|
||
|---|---|
|
||
| PC web JSON BFF | `https://www.acfun.cn/rest/pc-direct/...` |
|
||
| PC web **SSR-internal** BFF | `https://www.acfun.cn/rest/pc-node/...` — **[VERIFIED]** every call → `502`; not publicly routed |
|
||
| Mobile web JSON BFF | `https://m.acfun.cn/rest/mobile-direct/...` — **[VERIFIED]** `502` on `www.acfun.cn`, JSON on `m.acfun.cn` (**host-bound**) |
|
||
| Mobile web pages | `https://m.acfun.cn/communityCircle/...` |
|
||
| App API | `https://api-ipv6.acfunchina.com/rest/app/...` |
|
||
| App API (IPv6-less siblings) | `https://api.acfun.cn` → `502` **[VERIFIED]**; `app.acfun.cn` / `ias.acfun.cn` / `zt.acfun.cn` → DNS/connect failure |
|
||
| Like / interact service | `https://kuaishouzt.com/rest/zt/...`, `https://api.kuaishouzt.com/rest/zt/...` |
|
||
| Auth token service | `https://id.app.acfun.cn/rest/web/token/get` |
|
||
| Captcha / QR | `https://www.acfun.cn/rest/c/infra/captcha/{get,verify}`, `/rest/c/infra/ks/qr/*` (from bundles) |
|
||
|
||
**[VERIFIED]** The PC bundle states the prefix rule verbatim — this is why `pc-node` exists:
|
||
|
||
```js
|
||
function p(){return "server"===e.env.VUE_ENV||a.a.prototype.$isServer ? "/rest/pc-node" : "/rest/pc-direct"}
|
||
```
|
||
|
||
A Node plugin is **not** the AcFun SSR server, so it must use `/rest/pc-direct` (or `/rest/mobile-direct`
|
||
on `m.acfun.cn`, or `/rest/app` on the app host). `/rest/pc-node/*` is unusable from outside.
|
||
|
||
**Recommended headers** (what the mobile pages answer best to — see §3.4 for the `Accept` quirk):
|
||
|
||
```http
|
||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
|
||
Referer: https://www.acfun.cn/ (or https://m.acfun.cn/ for mobile hosts)
|
||
Accept: */*
|
||
Content-Type: application/x-www-form-urlencoded; charset=UTF-8 # for POSTs
|
||
```
|
||
|
||
No `sign`/`__`/`md5` parameter is needed on any read verified here. Writes need the midground token (§5).
|
||
|
||
---
|
||
|
||
## 2. Feature map — what actually exists in 2026
|
||
|
||
### 2.1 Page routes (all fetched live)
|
||
|
||
| URL | Result | Verdict |
|
||
|---|---|---|
|
||
| `https://www.acfun.cn/moment/` | `302 → /404` (52-byte body) | **dead** |
|
||
| `https://www.acfun.cn/moment` | `302 → /404` | **dead** |
|
||
| `https://www.acfun.cn/moment/am:1` | `302 → /404` | **dead** (the `/moment/am:momentId` route string still sits in an old bundle) |
|
||
| `https://www.acfun.cn/moment/square` | `302 → /404` | **dead** |
|
||
| `https://www.acfun.cn/square`, `/community`, `/discovery`, `/dynamic`, `/feed`, `/am`, `/index/moment`, `/square/` | `502` + 18 366-byte "出错啦!" page | **nonexistent** (`502` is AcFun's unknown-route response on www) |
|
||
| `https://www.acfun.cn/member/feeds`, `/member/` | `302 //www.acfun.cn?returnUrl=https%3A%2F%2Fwww.acfun.cn%2Fmember` | exists, **login-gated** (个人动态中心) |
|
||
| `https://www.acfun.cn/u/1` | `200`, 73 523 B | user space — tabs are 投稿/收藏/关注/粉丝; **no 动态 tab**, zero occurrences of `moment`/`动态` |
|
||
| `https://www.acfun.cn/` | `200`, 155 119 B, 8 bigPipe pagelets (`pagelet_player_info, pagelet_navigation, pagelet_live, pagelet_spring_festival, pagelet_monkey_recommend, pagelet_top_area, pagelet_banner, pagelet_list_banana`) | nav = 首页 / 直播 / 番剧 / video channels / 排行榜 — **no 广场, no 动态** |
|
||
| `https://m.acfun.cn/` | `302 → http://www.acfun.cn` | mobile home redirects to www |
|
||
| `https://m.acfun.cn/communityCircle` | `200` (31–40 KB shell) | 社区圈 index, **client-rendered** (no tags in HTML) |
|
||
| `https://m.acfun.cn/communityCircle/square` | `200` shell | 话题广场, **client-rendered, no tags in HTML** |
|
||
| `https://m.acfun.cn/communityCircle/tops`, `/tag` | `200` shell | same |
|
||
| `https://m.acfun.cn/communityCircle/{tagId}` | `200`, **144 KB, 15 moment items SSR'd** | **the working 广场 read surface** (§3.4) |
|
||
| `https://m.acfun.cn/communityCircle/moment/{momentId}` | `200`, `<title>Acfun 动态详情</title>`, contains `.moment-text`, `.moment-img-box`, `#comment` | **canonical human-readable moment page** |
|
||
| `https://m.acfun.cn/moment/200` | 404 page (HTTP 200) | wrong path |
|
||
|
||
### 2.2 JS bundle evidence (www, 40 bundles / 2.7 MB scanned)
|
||
|
||
Only **one** www bundle mentions `moment`: `https://ali-imgs.acfun.cn/kos/nlav10360/static/js/app.11fc9e51.js`
|
||
(the personal-center/header SPA). It contains the complete moment *store*, the feed *list* actions, and
|
||
the interaction *actions* — but **no publish action and no moment page component**. Extracted facts:
|
||
|
||
```js
|
||
// moment store normalisation — THE key mapping
|
||
n.resourceType = 10; // moments are resourceType 10
|
||
n.resourceId = n.momentId;
|
||
|
||
// moment detail
|
||
GetMomentDetail: function(e,t){
|
||
return a.a.get(Object(a.b)(e.rootState, Object(a.e)() + "/moment/detail?momentId=" + t), …)
|
||
.then(function(t){ … if(0===n.result) e.commit(f.CHANGE_MOMENT_DETAIL,{moment:n.moment, repostSource:n.repostSource}) })
|
||
}
|
||
|
||
// following feed list
|
||
GetFeedsFlow: function(e,t){ var n=t.pcursor||0, o=t.pageSize||20, c=t.resourceTypes||0;
|
||
… a.a.get(prefix + "/feed/followFeedV2?pcursor="+n+"&count="+o+"&resourceTypes="+c) …
|
||
// resourceTypes === 0 -> ChangeFeedsFlow (all), else -> ChangeFeedsArticleFlow (articles only)
|
||
}
|
||
|
||
// like / unlike
|
||
var h = "like"===u ? "/rest/zt/interact/add" : "/rest/zt/interact/delete";
|
||
a.a.post("//"+ztDomain+h, qs.stringify({
|
||
objectId:n, objectType:o, userId:d,
|
||
"acfun.midground.api_st":s, kpn:"ACFUN_APP", kpf:"PC_WEB", subBiz:"mainApp", interactType:1
|
||
}), {headers:{"Content-Type":"application/x-www-form-urlencoded"}})
|
||
// success is checked as: 1 === t.data.result
|
||
|
||
// banana on a moment
|
||
FeedThrowBanana: function(e,t){ var n=t.resourceId,r=t.resourceType,o=t.count;
|
||
a.a.post(prefix + "/banana/throwBanana", qs.stringify({resourceId:n, resourceType:r, count:o}), …)
|
||
// success is checked as: 0 === n.data.result
|
||
}
|
||
|
||
// other endpoints present in the same bundle
|
||
"/feed/followFeedV2?pcursor=0&count=5&resourceTypes=0"
|
||
"/feed/followDougaFeed?pcursor="
|
||
"/feed/webPush?count=50&pcursor=0"
|
||
"/feed/favorite/bangumi?count=10"
|
||
```
|
||
|
||
Routes in that bundle: `/member/feeds`, `/member/feeds/fans`, `/member/feeds/following`,
|
||
`/moment/am:momentId` (legacy string), `/moments/...` none. No publish/edit/delete anywhere.
|
||
|
||
---
|
||
|
||
## 3. Read contracts (each one verified live)
|
||
|
||
### 3.1 Moment detail — **the one reliable read** **[VERIFIED]**
|
||
|
||
```
|
||
GET https://www.acfun.cn/rest/pc-direct/moment/detail?momentId={momentId}
|
||
```
|
||
|
||
- Method/params: `GET`, single query param `momentId` (numeric). **Anonymous works.** No cookie needed.
|
||
- Also served identically by `https://api-ipv6.acfunchina.com/rest/app/moment/detail?momentId=…` and
|
||
`https://m.acfun.cn/rest/mobile-direct/moment/detail?momentId=…` **[VERIFIED]** (all three → `result:0`).
|
||
|
||
Live, trimmed (`momentId=5093831`, current-era content):
|
||
|
||
```json
|
||
{"result":0,"host-name":"hb2-acfun-kce-node24.aliyun","requestId":"eac1ac4e8c4e4fbd8d82a662927e7cc2",
|
||
"moment":{
|
||
"momentId":"5093831","momentType":2,"createTime":"2026-09-23",
|
||
"text":"#教育##父亲##贷款#","replaceUbbText":"#教育##父亲##贷款#",
|
||
"user":{"id":"23240229","name":"红格男(RSM)",
|
||
"headUrl":"https://tx-free-imgs.acfun.cn/newUpload/23240229_cfb0100bc9a64c0aa96d8…"},
|
||
"imgs":[{"w":1080,"h":2554,"type":4,"originUrl":"https://tx-free-imgs.acfun.cn/newUpload/23240229_44bf454add3…"}],
|
||
"likeCount":13,"commentCount":6,"bananaCount":5,"shareCount":0,
|
||
"isLike":false,"isThrowBanana":false,"visibleForFans":false,"originResourceType":0,
|
||
"tagList":[{"tagId":40887,"tagName":"贷款"},{"tagId":488,"tagName":"教育"},{"tagId":1968,"tagName":"父亲"}],
|
||
"shareUrl":"https://m.acfun.cn/communityCircle/moment/5093831"}}
|
||
```
|
||
|
||
Top-level keys: `result, host-name, requestId, moment` — plus `repostSource` **only when the moment is a
|
||
repost** (then counts/author live under `repostSource`, per the store mutation).
|
||
|
||
#### Moment id space — two eras **[VERIFIED]**
|
||
|
||
`moment/detail` distinguishes "not found" reasons, which lets you map the id space:
|
||
|
||
| `momentId` | `result` | `error_msg` |
|
||
|---|---|---|
|
||
| `1`, `100` | `140005` | `动态不存在或者已被删除` (in-range, deleted) |
|
||
| `10000`, `1000000`, `10000000`, `100000000`, `48910450`, `48896959` | `140002` | `内容不存在` (out of range) |
|
||
| `200`, `202`, `300`, `5093831`, `5093817` | **`0`** | real moments |
|
||
|
||
Binary search pinned the boundary: ids **≤ 7525** are "in range" (`140005`), ids **≥ 7526** are
|
||
`140002`. That range is the **legacy 2019 动态 system**: every live id found in `1..1200` is dated
|
||
`2019-05-23 … 2019-06-14`, user set is the 2019 launch crowd (七里香004, soso15, ACer第一次耳机 …),
|
||
`momentType` 1/2, text like "樱花" / "搞笑图片". The **current** system uses **7-digit ids** (`5093831`
|
||
→ dated `2026-09-23`, tag list with 话题 ids) and is reachable **only** by knowing the id (from a topic
|
||
page) — there is no enumerable list here.
|
||
|
||
So: `getMoment(id)` works for both eras, but ids cannot be discovered by the API alone.
|
||
|
||
### 3.2 Moment comments — **[VERIFIED]**
|
||
|
||
```
|
||
GET https://www.acfun.cn/rest/pc-direct/comment/listByFloor
|
||
?sourceId={momentId}&sourceType=4&page=1&supportZtEmot=true
|
||
```
|
||
|
||
**`sourceType = 4` identifies a moment.** Proof from a live matrix on `sourceId=202` (a moment whose
|
||
`commentCount` is 2):
|
||
|
||
| `sourceType` | HTTP | payload |
|
||
|---|---|---|
|
||
| **4** | 200 | `{"result":0,"curPage":1,"isUp":false,"sourceType":4,"totalPage":1,"totalCount":2, "commentsMap":{…},"commentIds":[…],"pageSize":50,…}` — **2 real comments** |
|
||
| 3 | 200 | `{"result":0,"sourceType":3,"totalPage":0,"totalCount":0,"commentsMap":{}}` (video type — empty) |
|
||
| 2 | 200 | `{"result":0,"sourceType":2,"totalPage":0,"totalCount":0}` (valid but unrelated) |
|
||
| 10 | 200 | `{"result":21,"error_msg":"参数格式错误,请仔细阅读API文档。"}` (invalid) |
|
||
|
||
Comment object keys **[VERIFIED]**:
|
||
`imageInfo, userHeadImgInfo, avatarFrameImgInfo, imageUrl, isDelete, quoteId, nameRed, nameColor, headUrl,
|
||
verifiedTypes, deviceModel, isSignedUpCollege, avatarFrame, avatarImage, sourceId, cid, isUpDelete, floor,
|
||
postDate, sourceType, userName, userId, isUp, verified, content, timestamp`
|
||
(e.g. `{"userName":"soso15","userId":"13360169","content":"666666"}`).
|
||
|
||
Note the **numbering split**, this is the classic trap: comments use `sourceType 4`, while
|
||
like/banana use `resourceType 10` (§4). They are different enums for the same object.
|
||
|
||
`/rest/mobile-direct/comment/list` also exists on `m.acfun.cn` (`result:21` without params → route alive)
|
||
but its param names (`resourceId`/`resourceType`) were rejected in probing — **[UNVERIFIED]**; use the
|
||
`pc-direct` `listByFloor` form above, which is fully verified.
|
||
|
||
### 3.3 Following feeds (the "动态流") — route **[VERIFIED]**, data login-scoped
|
||
|
||
```
|
||
GET https://www.acfun.cn/rest/pc-direct/feed/followFeedV2?pcursor=0&count=20&resourceTypes=0
|
||
GET https://www.acfun.cn/rest/pc-direct/feed/followDougaFeed?pcursor=0&count=20
|
||
GET https://www.acfun.cn/rest/pc-direct/feed/webPush?count=50&pcursor=0
|
||
GET https://www.acfun.cn/rest/pc-direct/feed/favorite/bangumi?count=10
|
||
```
|
||
|
||
Anonymous live results — **note these are `200`, not auth errors**, because the routes are
|
||
guest-tolerant and simply have nothing to show a guest with `followingCount: 0`:
|
||
|
||
```json
|
||
// followFeedV2
|
||
{"result":0,"feedList":[],"pullCount":0,"userInfo":{"followingCount":0},
|
||
"requestId":"ea7f66cd511f4926bf9e25c55435415a","followTags":[],"pcursor":"no_more","ups":[],"upsPos":0}
|
||
// followDougaFeed
|
||
{"result":0,"feedList":[],"userInfo":{"followingCount":0},"followUpers":[],"pcursor":"no_more","pageSize":10,"favBangumis":[]}
|
||
// webPush
|
||
{"result":0,"feedList":[],"followUpers":[],"pcursor":"no_more"}
|
||
// favorite/bangumi -> AUTH ERROR
|
||
{"result":-401,"error_msg":"user not login"}
|
||
```
|
||
|
||
So `followFeedV2` is the **right endpoint for "关注动态"** and it *does* carry moments in `feedList`
|
||
(the store action names them `feeds`, and the item schema is the same `resourceType 10` object), but
|
||
its content is entirely a function of the logged-in user's follows. **It is useless anonymously and
|
||
it is not a 广场.**
|
||
|
||
Params: `pcursor` (opaque cursor; `"no_more"` = end), `count` (default 20 in JS), `resourceTypes`
|
||
(`0` = all → `ChangeFeedsFlow`; non-zero → article flow, per the bundle's ternary).
|
||
|
||
The app API mirrors these **[VERIFIED]**: `/rest/app/feed/followFeed` → `200`
|
||
`{"result":0,"feedList":[],"pageNo":1,"totalPage":0,"pageSize":10}`, `/rest/app/feed/followFeedV2`
|
||
→ `200` same shape, `/rest/app/feed/followDougaFeed` → `result:11`.
|
||
|
||
### 3.4 Topic page (社区圈) — the only public "广场" read, **HTML only** **[VERIFIED]**
|
||
|
||
```
|
||
GET https://m.acfun.cn/communityCircle/{tagId}
|
||
```
|
||
|
||
Live (`tagId=488`, 教育): `200`, **144 110 bytes**, title `Acfun【教育】话题,快来参与`,
|
||
topic header `教育` + `38关注 | 10458内容`, and **15 server-rendered moment items**, newest dated
|
||
`2026-10-13` — i.e. this subsystem is genuinely in use today.
|
||
|
||
Item markup (stable, scrape-able):
|
||
|
||
```html
|
||
<li class="resources-item ks-1px-b type-1"
|
||
moment-id="" resource-type="1" resource-id="48910450" data-repost="false">
|
||
<div class="resources-user">…<p class="user-name">c137牛奶娘</p><p class="release-time">2026-10-13</p>…</div>
|
||
<div class="resources-article"><p class="article-title">…</p><p class="article-body">…</p></div>
|
||
<div class="resources-tag pics-0"><a class="tag-item" href="/communityCircle/488">#教育</a>…</div>
|
||
<div class="resources-base">…<span class="base-text">228</span>…</div>
|
||
</li>
|
||
```
|
||
|
||
Observed `resource-type` values and their `moment-id`:
|
||
|
||
| `resource-type` | `moment-id` | meaning |
|
||
|---|---|---|
|
||
| `1` | `""` (empty) | 图文动态/article-style post; only `resource-id` (e.g. `48910450`) — **not** a moment id |
|
||
| `2` | `""` | video-style post; only `resource-id` |
|
||
| `3` | **`5093831`, `5093817`** | **real moment** — this is how you harvest current moment ids |
|
||
|
||
⚠️ **The `Accept` header flips the renderer** — this cost me a lot of time, so it is worth stating
|
||
precisely. Same URL, two outcomes:
|
||
|
||
| Request headers | Length | Items |
|
||
|---|---|---|
|
||
| default `Accept` (or none, or `Referer` www vs m — irrelevant) | 144 110 | **15** ✅ |
|
||
| `Accept: text/html,application/xhtml+xml…` | 31 628 | **0** (JS shell) |
|
||
| `User-Agent: …Googlebot…` | 41 722 | 15 (different shell) |
|
||
|
||
So a Node `fetch` must send **default/`*/*` Accept** to get the SSR'd list, and must **not** pretend to be a
|
||
browser navigation.
|
||
|
||
⚠️ **No pagination.** `?page=2`, `?page=3`, `?sort=reply`, `?sort=post` all return the *identical* 15
|
||
items (byte-identical length, same first `resource-id=48910450`). The `热门排序 / 回复时间 / 发帖时间`
|
||
filter and the infinite scroll are client-side-only and their data source is **not** reachable:
|
||
`/communityCircle/square`, `/tops`, `/tag`, `/communityCircle` are all empty shells with **zero** tag
|
||
entries or `/rest/` URLs in the HTML.
|
||
|
||
### 3.5 `feed/hot` — **[VERIFIED]** but it is **videos, not moments**
|
||
|
||
```
|
||
GET https://www.acfun.cn/rest/pc-direct/feed/hot (also /rest/mobile-direct/feed/hot, /rest/app/feed/hot)
|
||
```
|
||
|
||
`200` `{"result":0,"feeds":[…], "host-name":…, "requestId":…}` — 9–14 items, **every one `type: 2`**
|
||
(douga video), with `dougaId`/`videoId`/`caption`/`commentCount`/`bananaCount` etc. It is the hot
|
||
**video** module. `count`, `resourceTypes`, `type`, `pcursor` are **all ignored** (same 9–14 items each
|
||
time, no `pcursor` in the response). Do not mistake it for a 广场 moment feed — but it is a fine
|
||
anonymous "hot feed" and the item schema is the same feed-item envelope that moments use.
|
||
|
||
---
|
||
|
||
## 4. Interaction endpoints (contracts from the site's own JS; writes only probed to their auth error)
|
||
|
||
> I have no account, so each write below was attempted **unauthenticated only** to prove the route and
|
||
> observe the auth tier. **No like/banana/comment/follow/post was ever actually performed.**
|
||
|
||
### 4.1 Like / unlike a moment **[UNVERIFIED — route + auth tier verified]**
|
||
|
||
```
|
||
POST https://kuaishouzt.com/rest/zt/interact/add # like
|
||
POST https://kuaishouzt.com/rest/zt/interact/delete # unlike
|
||
Content-Type: application/x-www-form-urlencoded
|
||
objectId={momentId}&objectType=10&userId={authorUserId}
|
||
&acfun.midground.api_st={token}&kpn=ACFUN_APP&kpf=PC_WEB&subBiz=mainApp&interactType=1
|
||
```
|
||
|
||
- `objectType` = the moment's `resourceType` = **10** (from the store's `n.resourceType=10`).
|
||
- Success is `{"result":1}` (note: **1**, not 0 — this endpoint's envelope differs from `pc-direct`).
|
||
- Auth tier: **signed midground token**. **[VERIFIED]** unauthenticated call →
|
||
`401 {"result":-401,"error_msg":"token value error","error_id":""}`.
|
||
(Note `/rest/zt/interact/delete` is the *unlike* route — it is not "delete a moment".)
|
||
|
||
### 4.2 投蕉 (banana) on a moment **[UNVERIFIED — route + auth tier verified]**
|
||
|
||
```
|
||
POST https://www.acfun.cn/rest/pc-direct/banana/throwBanana
|
||
Content-Type: application/x-www-form-urlencoded
|
||
resourceId={momentId}&resourceType=10&count={1..5}
|
||
```
|
||
|
||
**[VERIFIED]** unauthenticated → `401 {"result":-401,"error_msg":"user not login"}` (same for
|
||
`resourceType=1`), i.e. route exists, **plain session cookie** tier. Success would be `result:0`.
|
||
|
||
### 4.3 Comment / reply on a moment **[UNVERIFIED]**
|
||
|
||
```
|
||
POST https://www.acfun.cn/rest/pc-direct/comment/add
|
||
objectId={momentId}&content={ubb text}&replyToCommentId={0|commentId}
|
||
```
|
||
|
||
Inferred from the verified comment *list* pairing (`sourceId`+`sourceType=4`) and the existing
|
||
video-comment contract in [`docs/acfun-api.md`](acfun-api.md) (§3), which documents `comment/add` taking
|
||
`objectId`. Whether a moment post also needs `sourceType=4` is **[UNVERIFIED]** — the mobile comment
|
||
widget constructs itself with `initCommentWidget({resourceId: momentId, resourceType: 4, …})`
|
||
(from `pages/momentDetail/index.*.js`), so send `sourceType=4` as well to be safe.
|
||
Auth tier: **plain session cookie** (`user not login`). Mobile equivalent that exists but whose params I
|
||
could not pin down: `POST https://m.acfun.cn/rest/mobile-direct/comment/add` → `result:11` **[VERIFIED route]**.
|
||
|
||
### 4.4 Follow the moment author **[UNVERIFIED]**
|
||
|
||
```
|
||
POST https://www.acfun.cn/rest/pc-direct/relation/follow # {targetUid} / {herdId}
|
||
```
|
||
(`/rest/pc-direct/relation/follow` and `/rest/pc-direct/relation/getGroups` are both present in the www
|
||
bundles.) Cookie tier.
|
||
|
||
### 4.5 Vote (投票动态) **[UNVERIFIED — route + auth tier verified]**
|
||
|
||
```
|
||
POST https://api-ipv6.acfunchina.com/rest/app/moment/vote
|
||
```
|
||
**[VERIFIED]** `401 {"result":-401,"error_msg":"token value error"}` → app token tier. The moment store
|
||
has an `UpdateFeedVote` mutation and the mobile page reads `vote__radio` DOM, so 投票 moments exist as
|
||
`momentType` variants; the web cannot drive them.
|
||
|
||
### 4.6 Repost / 转发 **[UNVERIFIED — no route found]**
|
||
|
||
The store has `updateFeedRepostFollow` / `updateFeedRepostFavorite` mutations and renders
|
||
`repostSource`, so reposts exist and **can be read** (a repost's payload has a `repostSource` sibling).
|
||
But **no repost/forward endpoint was found**: `/rest/pc-direct/moment/{repost,forward,share}` → `404`,
|
||
and `/rest/app/moment/repost` → `404`. Most likely reposting = `moment/add` with an origin field
|
||
(app-only). Treat as impossible for a plugin.
|
||
|
||
### 4.7 Publish / delete a 动态 **[UNVERIFIED — app-only, token-gated]**
|
||
|
||
```
|
||
POST https://api-ipv6.acfunchina.com/rest/app/moment/add # publish
|
||
POST https://api-ipv6.acfunchina.com/rest/app/moment/delete # delete
|
||
```
|
||
|
||
**[VERIFIED]** `POST moment/add` (any body: `content=test`, `momentId=1`, or empty) →
|
||
`401 {"result":-401,"error_msg":"token value error"}` — the handler ran, so the route is real; the
|
||
`GET` form returns `result:11` (`网络错误,请稍后再试`). `moment/delete` returns `result:11` on GET
|
||
(route alive). **Every** `/rest/pc-direct/moment/{add,create,publish,post,delete,remove,repost,forward,
|
||
share,like,unlike,vote,update,edit}` returns **HTTP 404 on www** — there is no web publish path at all,
|
||
which is exactly why the community wrote a userscript instead of using the site.
|
||
|
||
Image upload for a moment would go through `/rest/pc-direct/image/upload/getToken` +
|
||
`getUrlAfterUpload` (present in bundles) — **[UNVERIFIED]**, and pointless without a publish route.
|
||
|
||
---
|
||
|
||
## 5. Auth tiers, signing, captcha
|
||
|
||
| Tier | What it needs | Evidence seen |
|
||
|---|---|---|
|
||
| **Anonymous** | nothing | `moment/detail`, `comment/listByFloor`, `feed/followFeedV2`, `feed/hot`, `m.acfun.cn/communityCircle/*` all `200` |
|
||
| **Session cookie** | `auth_key` / `_did` / `csrfToken` cookies from a real login | `401 {"result":-401,"error_msg":"user not login"}` for `feed/favorite/bangumi`, `banana/throwBanana` |
|
||
| **Signed midground token** | `acfun.midground.api_st`, obtained from `https://id.app.acfun.cn/rest/web/token/get` with a **logged-in app/web session**; sent as a form field | `401 {"result":-401,"error_msg":"token value error"}` for `kuaishouzt/rest/zt/interact/add`, `/rest/app/moment/add`, `/rest/app/moment/delete`, `/rest/app/moment/vote` |
|
||
|
||
- **No HMAC/md5 request signing** was observed on any read, nor on `interact/add` (it sends plain form
|
||
fields plus the token). The "signature" is effectively just the midground token.
|
||
- **Anonymous visit cookies** are only `csrfToken` and `_did` — no session.
|
||
- **Captcha / identity**: `/rest/c/infra/captcha/{get,verify}` and `/rest/c/infra/ks/qr/{start,scanResult,
|
||
acceptResult}` exist for login/QR. Both the desktop and mobile pages load
|
||
`@ks/identity-verification` (`static/@ks/identity-verification.81c79f9a249d01283c29.js`), so publishing
|
||
may also require 实名/手机号 identity verification **on top of** the token — **[UNVERIFIED]**, but the
|
||
SDK's presence on the very pages that would post is a strong hint.
|
||
- Observed `result` codes relevant here: `0` ok · `1` ok for `interact/add` · `11` handler ran, context/
|
||
params rejected · `21` param format error · `-401` auth (cookie or token) · **`140002` 内容不存在** ·
|
||
**`140005` 动态不存在或者已被删除** · `129000` kuaishouzt "Server Error." (missing token entirely).
|
||
|
||
---
|
||
|
||
## 6. Field maps
|
||
|
||
### 6.1 Moment object (`moment/detail` → `moment`, and `feedList[]` items when `resourceType`/`type` is a moment)
|
||
|
||
| Field | Type | Meaning / note |
|
||
|---|---|---|
|
||
| `momentId` | string | **the id** — string, not number. Used by detail + comments (`sourceId`) |
|
||
| `momentType` | number | `1` text-only, `2` image/text, `3` vote/其他 (observed 1/2 live; `3` inferred from `/communityCircle/{tagId}` items whose `moments-type-3` markup carries a vote) |
|
||
| `text` | string | raw content, includes `#话题#` markers and newlines |
|
||
| `replaceUbbText` | string | same text after UBB processing |
|
||
| `createTime` | `YYYY-MM-DD` **string** | day precision only — no time-of-day |
|
||
| `user` | object | see 6.2 |
|
||
| `imgs[]` | array | `{width,height,size,type,url,expandedUrl,originUrl}`; `type` 1/3/4 observed (gif vs jpg vs png); `originUrl` is the full-size original, `url` is a 224×224 thumb (`imageView2/5/w/224/h/224`) |
|
||
| `imgInfos[]` | array | the newer `<ImageInfo>` envelope (`thumbnailImage/smallSharedImage/expandedImage/originImage`, each `{cdnUrls:[{url,freeTrafficCdn}]}`) — prefer `imgs[].originUrl` for a plugin |
|
||
| `likeCount` / `commentCount` / `bananaCount` / `shareCount` | number | counters; `bananaCount` = 投蕉 |
|
||
| `isLike` / `isThrowBanana` | boolean | always `false` anonymously; `true`/`false` when a cookie is present |
|
||
| `visibleForFans` | boolean | 粉丝可见 flag |
|
||
| `originResourceType` | number | `0` for a native moment; non-zero when it wraps another resource |
|
||
| `tagList[]` | array | `{tagId, tagName}` — the 话题 links; `tagId` feeds `/communityCircle/{tagId}` |
|
||
| `shareUrl` | string | `https://m.acfun.cn/communityCircle/moment/{momentId}` — **use this as the canonical URL** |
|
||
| `groupId` | string | per-request rendering group, not stable — ignore |
|
||
| `voteInfo` | object | present on 投票 moments only (referenced by the `UpdateFeedVote` mutation) |
|
||
| `repostSource` | object (sibling of `moment`) | on reposts: a second moment-shaped object with its own `user`/counts; the store reads `moment.repostSource.*` |
|
||
|
||
### 6.2 Author object (`moment.user`)
|
||
|
||
`id` (numeric string), `name`, `headUrl`, `headCdnUrls[]`, `userHeadImgInfo`, `avatarFrame`/`avatarFramePcImg`/
|
||
`avatarFrameMobileImg`, `signature`, `gender`, `fanCount`/`fanCountValue`, `followingCount`/
|
||
`followingCountValue`, `contributeCount`/`contributeCountValue`, `isFollowing`, `followingStatus`,
|
||
`isFollowed`, `href`, `action`, `nameColor`, `socialMedal`, `sexTrend`.
|
||
The store normalises this to `{userHead, userId, userName, liveId}` — if you cache, keep `id`+`name`.
|
||
|
||
### 6.3 Feed-item envelope (`feedList[]`, `feed/hot[]`)
|
||
|
||
`groupId, contentId, videoId, dougaId, sourceId, type, caption, title, description, coverUrl, coverUrls[],
|
||
coverCdnUrls[], displayPlayCount, viewCount, displayDanmakuCount, danmakuCount, likeCount, likeCountShow,
|
||
commentCount, commentCountRealValue, bananaCount, bananaCountShow, shareCount, stowCount, isLike,
|
||
isThrowBanana, isFavorite, isFollowing, createTime, createTimeMillis, contributeTime, playDuration,
|
||
durationMillis, videoList[] (part list with id/title/danmakuCount), tagList[], user, channel,
|
||
superUbb, status, expTag, videoSizeType, shareUrl, picShareUrl`
|
||
For a moment, `type`/`resourceType` is **10** and the moment body sits alongside as `moment` /
|
||
`repostSource` (per the store). `feed/hot` items are `type:2` videos.
|
||
|
||
---
|
||
|
||
## 7. Dead ends, 404s, and app-only
|
||
|
||
**[VERIFIED] 404 on `www.acfun.cn`** (route simply not served publicly):
|
||
`/rest/pc-direct/moment/{add,create,publish,post,delete,remove,repost,forward,share,like,unlike,vote,
|
||
update,edit}` · `/rest/pc-direct/feed/{publish,addMoment,square,recommend,circle,topic,list,getFeed,
|
||
feedList,queryFeed,recommendList,recommendMoment,discover,index,home,timeline,circleFeed,topicFeed,
|
||
hotMoment,userFeed,userMoment,user,contribute}` · `/rest/pc-direct/moment/{list,getList,userList,hot,
|
||
recommend,square}` · `/rest/pc-direct/{topic/*,communityCircle/*,tag/resources,resources/list,resource/list}`.
|
||
|
||
**[VERIFIED] 404 on `api-ipv6.acfunchina.com`**: `/rest/app/moment/{square,list,recommend,hot,feed,
|
||
timeline,publish,create,repost}` · `/rest/app/{square/*,circle/list,community/*,feed/{square,list,
|
||
recommend}}`.
|
||
|
||
**[VERIFIED] 404 on `m.acfun.cn`**: `/rest/mobile-direct/{topic/*,communityCircle/*,tag/*,moment/{list,
|
||
square,recommend,hot,add,publish},feed/{square,recommend,list},square/list,resources/list}`.
|
||
|
||
**502 (wrong host)**: `/rest/pc-node/*` on www; `/rest/mobile-direct/*` on www.
|
||
**HTML 200 (not JSON) on kuaishouzt.com**: `/rest/zt/{feed,moment,square}/*` — only
|
||
`/rest/zt/interact/{add,delete}` is a real JSON endpoint there.
|
||
|
||
**App-only / effectively impossible for a web plugin**: publishing a 动态, deleting one, reposting,
|
||
voting, and the app's 广场/推荐 timeline. All of these sit behind the midground token on
|
||
`api-ipv6.acfunchina.com` and have no web counterpart. There is **no public square/recommend moment
|
||
list API** on any host — after ~560 path probes this is a solid negative, not a gap in the search.
|
||
|
||
---
|
||
|
||
## 8. Feasibility and the recommended plugin surface
|
||
|
||
### 8.1 Verdict per capability
|
||
|
||
| Capability | Feasible? | Basis |
|
||
|---|---|---|
|
||
| Read one moment by id (text, images, tags, counts, author) | ✅ **reliable, anonymous** | `moment/detail` `result:0` on legacy + 2026 ids |
|
||
| Read a moment's comments | ✅ **reliable, anonymous** | `comment/listByFloor?sourceType=4` `totalCount:2` with real comments |
|
||
| Read a topic/话题's moments | ⚠️ **possible but HTML-only, first 15, no paging** | `m.acfun.cn/communityCircle/{tagId}` 144 KB SSR |
|
||
| Read the 关注动态 stream | ⚠️ **cookie only** | `feed/followFeedV2` `200` but empty for guests |
|
||
| Discover moment ids / browse a 广场 | ❌ **no API** | ~560 probes; topic SSR is the only discovery vector |
|
||
| Like/unlike | 🔶 **plausible, unverifiable** | route + token tier verified via `401` |
|
||
| 投蕉 | 🔶 **plausible, unverifiable** | route + cookie tier verified via `401` |
|
||
| Comment/reply | 🔶 **plausible, unverifiable** | list side verified; `comment/add` from existing doc |
|
||
| Follow author | 🔶 **plausible, unverifiable** | `/relation/follow` in bundles |
|
||
| Repost | ❌ | no route found |
|
||
| Publish / delete 动态 | ❌ **impossible for a web plugin** | app-only, midground token |
|
||
| Vote | ❌ | app-only, midground token |
|
||
|
||
### 8.2 Smallest useful implementation
|
||
|
||
Two read tools, both anonymous, plus one optional cookie-gated tool. Do **not** ship a "square feed" tool.
|
||
|
||
```
|
||
acfun_moment(momentId: string)
|
||
GET https://www.acfun.cn/rest/pc-direct/moment/detail?momentId={id}
|
||
-> { momentId, url, author{id,name,avatar}, text, createTime, images[], tags[{tagId,name}],
|
||
counts{like,comment,banana,share}, isRepost, repostSource? , raw }
|
||
Accepts a bare id, "am:5093831", or a https://m.acfun.cn/communityCircle/moment/5093831 URL.
|
||
Errors: 140002 -> NOT_FOUND (out of range); 140005 -> DELETED.
|
||
|
||
acfun_moment_comments(momentId, page=1)
|
||
GET https://www.acfun.cn/rest/pc-direct/comment/listByFloor
|
||
?sourceId={id}&sourceType=4&page={page}&supportZtEmot=true
|
||
-> { totalCount, totalPage, page, comments[{cid, floor, userName, userId, content, postDate, isUp}] }
|
||
|
||
acfun_topic_moments(tagId, limit<=15) # optional, mark as best-effort
|
||
GET https://m.acfun.cn/communityCircle/{tagId} with DEFAULT Accept (no text/html!)
|
||
Parse li.resources-item[resource-type=3] -> moment-id ; other types -> resource-id only.
|
||
-> { tag{id,name,followers,contents}, items[{momentId?, resourceType, resourceId, user, time, text, stats}] }
|
||
First page only; params page/sort do nothing.
|
||
|
||
acfun_following_feed(cursor) # only when a session cookie is configured
|
||
GET .../feed/followFeedV2?pcursor={cursor||0}&count=20&resourceTypes=0
|
||
-> { items[], nextCursor } ; nextCursor="no_more" terminates; empty list for anonymous.
|
||
```
|
||
|
||
Mapping to the existing plugin shape in this repo: these are pure `fetch` GETs, so they slot in beside
|
||
the existing `acfun_video` / `acfun_comments` tools with no auth plumbing (`lib/provider.js` style).
|
||
`acfun_moment` is the one to ship first — it is the only capability that is both anonymous and
|
||
complete. `acfun_topic_moments` is the only way to *discover* a moment id, so it earns its place
|
||
despite being HTML scraping.
|
||
|
||
### 8.3 Risk notes
|
||
|
||
1. **HTML scraping is load-bearing for discovery.** `communityCircle/{tagId}` has no API, no pagination,
|
||
and its SSR only appears when `Accept` is not `text/html`. A markup change breaks discovery, not
|
||
reads. Isolate the parser and fail soft.
|
||
2. **Two id spaces.** Legacy `1…7525` (2019, mostly deleted) and current 7-digit ids. Never
|
||
assume contiguity, never enumerate to "browse" — validate the id and surface `140002`/`140005`
|
||
distinctly (they mean different things).
|
||
3. **`sourceType 4` vs `resourceType 10`.** Comments use 4, like/banana use 10. Mixing them silently
|
||
yields empty comment lists (`sourceType 3` → `totalCount:0`) rather than an error.
|
||
4. **Host binding.** `/rest/mobile-direct/*` only answers on `m.acfun.cn`; `/rest/pc-node/*` never
|
||
answers publicly. Hard-coding the wrong pair gives `502`s that look like outages.
|
||
5. **Guest-tolerant ≠ available.** `feed/followFeedV2` returns `200` with an empty list for a guest;
|
||
a plugin must not report "no dynamics" when it means "not logged in".
|
||
6. **Writes cannot be validated at all here.** `interact/add` success is `result:1` while every other
|
||
route uses `result:0` — easy to get backwards. If write tools are ever enabled, require a cookie and
|
||
treat any non-`0`/`1` as failure, and never retry blindly.
|
||
7. **The feature is app-first and actively mutating.** www dropped `/moment/` entirely; the live
|
||
subsystem moved to `m.acfun.cn/communityCircle` and the app. Treat every endpoint here as
|
||
*verified on 2026-10-27* and re-probe before relying on it.
|
||
|
||
---
|
||
|
||
## 9. How each claim was produced (short reproduction log)
|
||
|
||
| # | Probe | What it established |
|
||
|---|---|---|
|
||
| p1 | fetch 7 candidate URLs, `redirect:'manual'` | `/moment/`→302 `/404`; `/square`,`/community`,`/discovery`→502 error page |
|
||
| p2 | home page bigPipe frames + keyword scan | 8 pagelets, nav routes, **0** occurrences of moment/square/dynamic |
|
||
| p3 | `pagelet_navigation` HTML + `/member/*` | nav = 首页/直播/番剧/channels/排行榜; `/member/feeds`→login redirect |
|
||
| p4 | `/u/1`, `/u/2` space pages | no 动态 tab, no moment strings |
|
||
| p5 | download 70 www bundles (2.7 MB), regex `/rest/` | 50 rest paths; **only** `app.11fc9e51.js` mentions moment |
|
||
| p6–p9 | dissect `app.11fc9e51.js` | prefix fn, `moment/detail`, `followFeedV2`, `banana/throwBanana`, `interact/add`, `resourceType=10` |
|
||
| p10 | live probe of 7 read + 2 write endpoints | detail 200, feeds 200-empty, `favorite/bangumi` 401, `throwBanana` 401, `interact/add` 401 token |
|
||
| p11 | existence oracle over 533 `/rest/pc-direct/{moment,feed,...}` paths (502 vs JSON) | only `moment/detail` + `feed/hot` are JSON |
|
||
| p12 | `feed/hot` shape + param matrix | 9–14 video items, `type:2` only, params ignored |
|
||
| p13–p14 | moment-id range scan + binary search | boundary 7525/7526; `momentId=200/300` → `result:0` |
|
||
| p15 | fetch + scan ids 1..1200 | all live moments dated 2019-05/06; full field list |
|
||
| p16 | `m.acfun.cn/communityCircle*` + app host | `/communityCircle/square` 200; `/moment/detail` on app host 200 |
|
||
| p17–p20 | mobile pagelets + 37 mobile bundles | pages client-rendered; comment widget uses `resourceType 4`, `mobile-direct` paths |
|
||
| p21 | moment variety + comment `sourceType` matrix | `sourceType=4` → 2 real comments; 3/2 empty; 10 → result 21 |
|
||
| p22–p23 | publish/repost oracle on www + m + app | www 404 all; `POST /rest/app/moment/add` → 401 token; `vote` → 401 |
|
||
| p24–p26 | topic pages | `/communityCircle/488` SSR 15 items dated 2026-10-13; tag 教育 38关注/10458内容 |
|
||
| p27–p29 | paging/sort/headers | `?page`/`?sort` no-ops; `Accept: text/html` kills the SSR list; `*/*` restores it |
|
||
| p30 | moment page SSR | `m.acfun.cn/communityCircle/moment/{id}` → `Acfun 动态详情` with `.moment-text` |
|
||
| p31–p32 | kuaishouzt + final list hunt | zt only has `interact/*`; no public/user moment list anywhere |
|
||
| p33 | clean evidence capture | the JSON/comment/tag snippets pasted above |
|