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
This commit is contained in:
tamakyi
2026-10-27 11:50:44 +08:00
parent f9bedde19e
commit 97825f020d
19 changed files with 3723 additions and 189 deletions
+600
View File
@@ -0,0 +1,600 @@
# 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 |