Files
tamakyi 97825f020d feat: QR login, AcFun settings panel, and 动态 (moment) tools
- 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
2026-10-27 11:50:44 +08:00

601 lines
37 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 |