Files
tamakyi f9bedde19e feat: AcFun tools plugin for DeepSeek Harness
Eight tools (search, video, comments, danmaku, rank, playurl, interact,
post_comment) over acfun.cn's public web API, a model-facing skill, a
bundle patch with a skill-filesystem row, and offline/live checks.

Reads are anonymous; writes require allowWriteActions plus a login cookie.
2026-10-27 11:26:51 +08:00

842 lines
38 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 (acfun.cn / "A站") Web API — reverse-engineered for a Node.js `fetch` plugin
All probes were executed live on **2026-10-27 (Asia/Shanghai)** against `https://www.acfun.cn`
using desktop Chrome UA and `Referer: https://www.acfun.cn/`. Every endpoint below is marked
**[VERIFIED]** (called live, real status/JSON pasted) or **[UNVERIFIED]** (contract only — usually
because it needs a logged-in session I do not have; writes were *never* executed).
---
## 0. Conventions, host map, and shared plumbing
### 0.1 Base hosts
| Purpose | Host |
|---|---|
| PC web JSON API | `https://www.acfun.cn` (paths under `/rest/pc-direct/...`) |
| Login / token service | `https://id.app.acfun.cn` (`/rest/web/token/get`) |
| Like ("顶") service | `https://kuaishouzt.com` (`/rest/zt/interact/add`), also `api.kuaishouzt.com` |
| App API (legacy, mostly dead) | `https://api-ipv6.acfunchina.com` (`/rest/app/...`) |
| Video CDN (m3u8/ts) | `https://tx-video.acfun.cn`, `https://tx-safety-video.acfun.cn`, `https://ali-safety-video.acfun.cn` |
### 0.2 Two different "video ids" — this is the #1 gotcha
The PC web uses **two numeric ids per video**:
| Name in payload | Meaning | Example | Used by |
|---|---|---|---|
| `dougaId` / `contentId` / `id` | The "ac number" — what appears in `ac12345678` and in `/v/acNNNN` URLs | `42588725` | page URL, comments (`sourceId`), favorites (`resourceId`), banana (`resourceId`), interact (`objectId`) |
| `videoId` / `currentVideoId` / `currentVideoInfo.id` | Per-part id (a video can have several parts) | `32224197` | playInfo (`videoId`), danmaku (`resourceId`) |
**[VERIFIED]** `/v/ac42588725` → HTTP 200, full SSR payload.
**[VERIFIED]** `/v/ac32224197` (using the *part* id in the URL) → **HTTP 404** (the app shell).
So a plugin's `getVideo(acId)` must treat the number the user typed as `dougaId`; the `videoId`
needed by playInfo/danmaku comes out of the detail payload (`currentVideoId`, `videoList[].id`).
### 0.3 Required request headers
**[VERIFIED]** JSON reads work **without** `Referer` and even with curl's default UA:
```
no-referer search HTTP 200
no-UA search HTTP 200 -> {"result":0,"totalNum":547,...}
```
Recommended (matches the browser, avoids surprises):
```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/
Accept: application/json, text/plain, */*
```
- `Content-Type: application/x-www-form-urlencoded; charset=UTF-8` for every POST listed here
(the site's own client sends form-encoded, **not** JSON, for `/rest/pc-direct/*`).
- **No `sign` / `__` / `md5` parameter is needed** on any read endpoint verified here. The site's
own comment widget (`@ac/pc-comment.*.js`) posts plain form data with
`subBiz=jimu&kpf=PC_WEB&kpn=ACFUN_APP` and no signature. (Auth token is a separate thing, §7.)
### 0.4 Common response envelope
```json
{"result": 0, "host-name": "hb2-acfun-kce-node20.aliyun"}
```
Observed `result` codes:
| `result` | HTTP | Meaning |
|---|---|---|
| `0` | 200 | success |
| `1` | 200 | generic "ok" for some interact endpoints (`/rest/zt/interact/add`) |
| `11` | 200 | `"网络错误,请稍后再试"` / `"请检查下网络连接是否正常"` — handler ran but request context/params rejected (e.g. POST to a route that wants different params) |
| `21` | 200 | `"参数格式错误,请仔细阅读API文档"` — missing/malformed parameter |
| `-401` | **401** | `"user not login"` — route exists, cookie session missing/invalid |
| `-401` | **401** | `"token value error"` — route exists, requires the `acfun.midground.api_st` token (§7) |
| `105001` | 200 | app API (`api-ipv6.acfunchina.com`) — auth token invalid/missing |
### 0.5 Auth cookies (names only — never logged in here)
**[VERIFIED]** an anonymous visit to `https://www.acfun.cn/` sets only:
```
.acfun.cn _did web_83374403393B0029
www.acfun.cn csrfToken Pkyh8iuCGfRggSGwYox04LCz
```
**[UNVERIFIED]** logged-in session (from community docs for `id.app.acfun.cn/rest/app/login/signin`
plus the PC web's own `getCookie("auth_key")` usage):
```
acPasstoken <- login response field acPassToken (the main session cookie)
auth_key <- login response field auth_key (numeric userId)
_userid <- numeric userId
acPlatform = 2 (PC web)
_did, csrfToken (set for anonymous too)
```
Probes with forged values (`acPasstoken=deadbeef; auth_key=123456; _userid=123456; acPlatform=2`)
still return `{"result":-401,"error_msg":"user not login"}` — proving the server validates the
session rather than merely checking cookie presence. **Minimum cookie set remains UNVERIFIED.**
---
## 1. Video detail — [VERIFIED] via SSR page (there is no working `pc-direct` JSON endpoint)
### 1.1 Endpoint
```
GET https://www.acfun.cn/v/ac{dougaId} # dougaId == contentId == the "ac" number
```
- HTTP **200**, `text/html`, ~139 KB.
- The page embeds the entire structured video object as a JS assignment:
```js
window.pageInfo = window.videoInfo = { ... }; // then: window.videoResource = {}; ...
```
Parse with a regex from `window.pageInfo = window.videoInfo = ` up to `window.videoResource`, trim
the trailing `;`, then `JSON.parse`. In PowerShell (what I actually ran):
```powershell
$c = Get-Content page.html -Raw
$s = $c.IndexOf('window.pageInfo = window.videoInfo = ') + 38
$e = $c.IndexOf('window.videoResource', $s)
$o = ($c.Substring($s, $e-$s).Trim().TrimEnd(';') | ConvertFrom-Json)
```
Node equivalent for the plugin:
```js
const html = await (await fetch(`https://www.acfun.cn/v/ac${acId}`, {headers})).text();
const start = html.indexOf('window.pageInfo = window.videoInfo = ');
const end = html.indexOf('window.videoResource', start);
const info = JSON.parse(html.slice(start + 38, end).trim().replace(/;\s*$/, ''));
```
**Evidence:** HTTP 200, payload length 15,567 chars, 63 top-level keys.
```
HTTP 200 https://www.acfun.cn/v/ac42588725
LEN 138828 (html) pageInfo JSON LEN 15567
TOP KEYS: currentVideoId, pctr, commentCountRealValue, groupId, bananaCountShow,
stowCountShow, giftPeachCountShow, stowCount, channel, likeCount, hasHotComment,
shareCountShow, isDislike, shareCount, picShareUrl, danmakuCount, currentVideoInfo,
viewCountShow, dougaId, commentCountTenThousandShow, coverImgInfo, commentCount,
coverUrl, disableEdit, createTimeMillis, superUbb, shareUrl, status, isFavorite,
isLike, description, title, belongToSpecifyArubamu, result, videoList, isThrowBanana,
viewCount, bananaCount, likeCountShow, coverCdnUrls, giftPeachCount, isRewardSupportted,
commentCountShow, durationMillis, host-name, tagList, danmakuCountShow, createTime,
user, mkey, priority
```
### 1.2 Field mapping (exactly what a plugin needs)
| Plugin field | Path | Sample (ac42588725) |
|---|---|---|
| ac number | `dougaId` | `42588725` |
| part/video id (for playInfo/danmaku) | `currentVideoId` | `32224197` |
| title | `title` | `【原神】三周年超治愈的歌曲《你呀你呀》` |
| description | `description` | `""` (many videos have empty) |
| author name | `user.name` | `天阶夜色凉如水de陈秀凤` |
| author id | `user.id` | `74428749` |
| author avatar | `user.headUrl` (also `user.headCdnUrls[].url`) | `…/defaultAvatar.jpg?imageslim` |
| author signature | `user.signature` | — |
| author fans | `user.fanCountValue` (number) / `user.fanCount` (display) | — |
| is following author | `user.isFollowing` | `false` |
| views | `viewCount` (`viewCountShow` display) | `5682` |
| danmaku count | `danmakuCount` (`danmakuCountShow`) | `8` |
| comments | `commentCount` (also `commentCountRealValue`) | `21` |
| likes/"顶" | `likeCount` (`likeCountShow`) | `217` |
| favorites/collect | `stowCount` (`stowCountShow`) | `324` |
| bananas/投蕉 | `bananaCount` (`bananaCountShow` string `"119"`) | `119` |
| shares | `shareCount` (`shareCountShow`) | `34` |
| publish time | `createTimeMillis` (ms epoch) / `createTime` (`"2023-10-8"`) | `1696772749620` |
| duration | `durationMillis` (ms) | `196976` (= 3:16) |
| cover | `coverUrl` (`coverImgInfo.thumbnailImage.cdnUrls[].url` variants) | `https://tx-free-imgs.acfun.cn/newUpload/…png?imageslim` |
| channel | `channel.id`, `channel.name`, `channel.parentId`, `channel.parentName` | `106 / 动画综合 / 1 / 动画` |
| tags | `tagList[]` → `{id, name}` | `[{id:"609267",name:"原神空"},{id:"371390",name:"#原神"},{id:"9546",name:"原神"}]` |
| parts | `videoList[]` → `{id, title, durationMillis}` | `[{id:"32224197",title:"noTitle",durationMillis:196976}]` |
| playback (embedded!) | `currentVideoInfo.ksPlayJson` / `ksPlayJsonHevc` | see §4 |
| viewer state | `isLike`, `isFavorite`, `isThrowBanana`, `isFollowing` | all `false` anonymously |
| video status | `status` | `2` |
### 1.3 Endpoint status (dead paths)
**[VERIFIED 404]** — all return the SPA 404 HTML (~102.5 KB `<!DOCTYPE html>` shell):
```
/rest/pc-direct/video/index?videoId=32224197
/rest/pc-direct/video/detail?videoId=32224197
/rest/pc-direct/video/getVideoInfo?videoId=32224197
/rest/pc-direct/video/info?videoId=32224197
```
Caveat: the SSR payload is the only *complete* detail source. The search API (§2) can supply a
partial substitute (title/author/avatar/counts/cover/duration/channelId/ctime) but has **no
description and no tag names**, and it only works if the video is searchable.
---
## 2. Video search — [VERIFIED]
### 2.1 Endpoint
```
GET https://www.acfun.cn/rest/pc-direct/search/video?keyword={urlencoded}&page=1&pageSize=20[&sortType=N]
```
Also accepts **POST** with the same form fields (identical JSON result). No referer/auth needed.
**Evidence (baseline, the example that already worked):**
```
HTTP 200 len 23026
{"result":0,"totalNum":600,"videoList":[{...}]}
```
### 2.2 Parameters — and the pagination trap
| Param | Status | Notes |
|---|---|---|
| `keyword` | **[VERIFIED]** required, URL-encoded UTF-8 | `keyword=原神` → 600 hits |
| `page` | **[VERIFIED] ignored** | `page=1/2/3` returns the *identical* 30 rows (first id always `32224197`) |
| `pageSize` | **[VERIFIED] ignored** | `pageSize=5/10/20/50` all return exactly **30** rows |
| `pageNum`, `pageNo`, `pn`, `offset`, `cursor`, `pcursor`, `currentPage` | **[VERIFIED] ignored** | all identical results (tried each) |
| `sortType` | **[VERIFIED] honoured** | `0`/`1`/omitted = relevance (default); **`2` = most-viewed** (ordering by `viewCount` desc: `29725, 29452, 8253, 7443, 5682`); `3`/`4` behaved like `2` |
Evidence for ignored pagination:
```
pageSize=5 -> HTTP 200 count=30 total=600
pageSize=20 -> HTTP 200 count=30 total=600
page=2 -> HTTP 200 count=30 first=32224197 (same as page 1)
GET 龙 page1 sort2 -> 200 total=600 count=30 first=17777112
GET 龙 page2 sort2 -> 200 total=600 count=30 first=17777112
POST (page=2) -> 200 len=22040 (same shape; page still not applied)
```
The response echoes bogus pagination metadata (`pageNum=20`, `pageSize=30` regardless of input),
which is a server-side bug. **Conclusion: only the first 30 results are obtainable.** Deep search
paging must go through the SSR search page (`/search?keyword=...&page=N`, server-rendered) instead.
`totalNum` sanity check: obscure keyword `qzxwvn` → `totalNum=0`, `videoList=[]`; `test` → 547;
common terms (`原神`, `编程`, `龙`) → `600` (appears to be a display cap at 600).
### 2.3 Response field map (`videoList[]`)
```
top-level keys: result, totalNum, videoList, requestId, pageSize, host-name, pageNum
videoList[0] keys: groupId, emTitle, viewCountInfo, danmakuCountInfo, displayInfo,
belongToSpecifyArubamu, channelId, viewCount, commentCount, coverUrl, userImg, videoId,
contentId, danmuCount, playDuration, userName, userId, id, itemType, ctime, title
```
| Plugin field | Path | Sample |
|---|---|---|
| ac number | `contentId` (== `id`) | `42588725` |
| part id (for playInfo/danmaku) | `videoId` | `32224197` |
| title | `title` | `【原神】三周年超治愈的歌曲《你呀你呀》` |
| title with `<em>` highlights | `emTitle` | `【<em>原神</em>】…` |
| author name | `userName` | `天阶夜色凉如水de陈秀凤` |
| author id | `userId` | `74428749` |
| author avatar | `userImg` | `https://tx-free-imgs.acfun.cn/…?imageslim` |
| views | `viewCount` / `viewCountInfo` | `5682` / `"5682次播放"` |
| comments | `commentCount` | `21` |
| danmaku | `danmuCount` / `danmakuCountInfo` | `8` |
| duration | `playDuration` (`"mm:ss"` string!) | `"03:16"` |
| published | `ctime` (ms epoch) | `1696772749620` |
| channel | `channelId` (id only — no name!) | `106` |
| cover | `coverUrl` | `https://tx-free-imgs.acfun.cn/newUpload/…png?imageslim` |
| type | `itemType` | `2` (video) |
| display string | `displayInfo` | `"5682次播放 2023-10-08"` |
| short description | `decr` (some items only) | — |
### 2.4 Sibling search endpoints
**[VERIFIED 200]** `GET /rest/pc-direct/search/suggest?keyword=...` — used by the header search box
(found in `common/widget/searchBox/index.*.js`).
**[VERIFIED 200, documented]** `GET /rest/pc-direct/search/user?keyword=...` (returns `userList`).
---
## 3. Comments (评论 / 回复) — [VERIFIED]
### 3.1 Main comment list (new "JIMU" comment system — works anonymously)
```
GET https://www.acfun.cn/rest/pc-direct/comment/listByFloor
?sourceId={dougaId}&sourceType=3&page=1&supportZtEmot=true
```
- `sourceId` = the **ac number** (`dougaId`), *not* `videoId`.
- `sourceType` = **3** for videos (the embedded comment objects themselves carry
`"sourceType": 3`, which is the authoritative value). Lenient server quirk: `sourceType=1`
also returned the same 21 comments; `sourceType=2` returned 0. Use **3**.
- `supportZtEmot=true` enables Kuaishou emoticon rendering in `content`.
- Optional `pivotCommentId` / `newPivotCommentId` / `t` (cache-buster ms) accepted.
**Evidence (HTTP 200):**
```json
{"result":0,"curPage":1,"isUp":false,"sourceType":3,"totalPage":1,
"pivotCommentId":0,"commentsMap":{...},"contentUbbVersion":"1.0",
"pageSize":50,"totalCount":21,"commentIds":["c806796787", ...]}
```
`totalCount=21` matches `commentCount`/`commentCountRealValue` in pageInfo. `pageSize` is 50,
`totalPage` 1. `commentsMap` is an **object keyed by `"c" + cid`** (`"c806796787"`), and
`commentIds[]` lists those keys in display order (newest first).
**Comment object field map** (from `commentsMap["c…"]`):
```
userHeadImgInfo, avatarFrameImgInfo, imageInfo, cid, userId, isUpDelete, floor, postDate,
isUp, userName, sourceType, verified, sourceId, isDelete, headUrl, nameRed, avatarFrame,
quoteId, nameColor, verifiedTypes, deviceModel, isSignedUpCollege, avatarImage, imageUrl,
content, timestamp
```
| Plugin field | Path | Sample |
|---|---|---|
| comment id | `cid` | `806796787` |
| author id / name | `userId` / `userName` | `77984979` / `不知何处吊湘君de段倩玉` |
| avatar | `headUrl[]` → `{cdn,url}`; also `userHeadImgInfo.thumbnailImage.cdnUrls[].url` | `https://tx-free-imgs.acfun.cn/…` |
| content (UBB, may contain `[emot=acfun,ID/]`) | `content` | `[emot=acfun,2789/][emot=acfun,2789/]…` |
| floor | `floor` | `10` |
| posted | `timestamp` (ms) / `postDate` (`"07-28 21:32"` local, no year) | `1785245573037` |
| is uploader | `isUp` | `false` |
| verified badge | `verified` / `verifiedTypes[]` | `0` |
| name color | `nameColor`, `nameRed` | `0` |
| deleted markers | `isDelete`, `isUpDelete` | `false` |
| device | `deviceModel` | `HUAWEI ART-AL00x` |
| quote | `quoteId` | `0` |
| source | `sourceId`, `sourceType` | `42588725`, `3` |
Note: `listByFloor` comment objects have **no** `likeCount` / `subCommentCount` fields; reply
counts come from the sublist call (§3.2). Total count comes from `totalCount` on the list response.
### 3.2 Reply / sub-comments — [VERIFIED]
```
GET https://www.acfun.cn/rest/pc-direct/comment/sublist
?sourceId={dougaId}&sourceType=3&rootCommentId={cid}&page=1&supportZtEmot=true
```
**Evidence (HTTP 200):**
```json
{"result":0,"subCommentCount":0,"curPage":1,"subComments":[],"sourceType":3,
"totalPage":0,"contentUbbVersion":"1.0","pcursor":"no_more","pageSize":10,
"contentTitle":"【原神】三周年超治愈的歌曲《你呀你呀》",
"rootComment":{...}}
```
`subComments[]` items follow the same shape as §3.1 (they additionally carry `subCommentCount` /
reply-level fields). `pageSize=10`, `pcursor` ∈ `{number, "no_more"}`.
### 3.3 Dead / auth-gated comment routes
**[VERIFIED]** `POST /rest/pc-direct/comment/list` (`objectId`+`pcursor`+`pageSize`,
the "SECONDARY"/legacy widget path) — **does not work anonymously**:
```
POST /rest/pc-direct/comment/list (objectId=42588725&pcursor=&pageSize=20&subBiz=jimu&kpf=PC_WEB&kpn=ACFUN_APP)
-> HTTP 200 {"result":11,"error_msg":"网络错误,请稍后再试"}
GET /rest/pc-direct/comment/list?objectId=…&objectType=…&page=…&pageSize=…
-> HTTP 200 {"result":21,"error_msg":"参数格式错误,请仔细阅读API文档。"}
```
`/comment/subCommentList`, `/comment/hotList`, `/comment/replyList` → **[VERIFIED 404]** (SPA shell).
The legacy `objectType` parameter does not exist in the new widget at all (0 occurrences in
`@ac/pc-comment.*.js`); classification is by `sourceId`+`sourceType` only.
---
## 4. Playback (playInfo / m3u8 / quality list) — [VERIFIED]
### 4.1 playInfo endpoint
```
GET https://www.acfun.cn/rest/pc-direct/play/playInfo/ksPlayJson?videoId={videoId}
```
(POST with the same form body also works. `videoId` is the **part id** from pageInfo
`currentVideoId`, not the ac number. Extra `resourceId`/`resourceType` params are ignored.)
**Evidence (HTTP 200, 7,494 bytes):**
```json
{"result":0,"playInfo":{
"streams":[],
"ksPlayJsonHevc":"{\"version\":\"1.0.0\",\"businessType\":1,...}",
"domainInfos":…, "duration":…, "sizeType":…, "firstFrameUrls":…,
"userPlayedSeconds":0, "danmakuGuidePosition":…, "durationMillis":196976,
"ksPlayJson":"{\"version\":\"1.0.0\",\"businessType\":1,\"mediaType\":1,
\"videoId\":\"a7856caac8f5d4cc\",\"hideAuto\":false,\"adaptationSet\":[{
\"id\":0,\"duration\":196976,\"representation\":[
{\"id\":1,\"url\":\"https://tx-video.acfun.cn/mediacloud/acfun/acfun_video/a7856caac8f5d4cc-…-hls_1080p_h264_6m_1.m3u8?sign=6971e530669cf53e31dbf8a424d4acc6&t=1793091872&us=2128358904\",
\"backupUrl\":[\"https://ali-safety-video.acfun.cn/…\"],\"maxBitrate\":6000,\"avgBitrate\":1483,
\"width\":1920,\"heigh\":1080,\"qualityType\":\"1080p+\",\"qualityLabel\":\"1080P+\",…}]}]}",
"isKsManifest":…, "transcodeInfos":[…]},
"host-name":"hb2-acfun-kce-node20.aliyun"}
```
Parse: `JSON.parse(playInfo.ksPlayJson)` → `adaptationSet[0].representation[]`.
**Quality list actually returned for this video (HTTP 200):**
| id | qualityType | qualityLabel | width×height | maxBitrate | avgBitrate |
|---|---|---|---|---|---|
| 1 | `1080p+` | 1080P+ | 1920×1080* | 6000 | 1483 |
| 2 | `1080p` | 1080P | 1920×1080* | 3000 | 1143 |
| 3 | `720p` | 720P | 1280×… | 2000 | 779 |
| 4 | `540p` | 540P | 960×… | 900 | 431 |
| 5 | `360p` | 360P | 640×… | 500 | 288 |
\* the JSON field is misspelled `heigh` (not `height`) — read `rep.heigh`.
Page-level **quality catalogue** is also embedded in the HTML as
`window.qualityConfig` (all 16 tiers the player can negotiate):
`2160p120HDR, 2160p120, 2160p60HDR, 2160p60, 2160pHDR, 2160p, 1080p60HDR, 1080p60, 1080p+,
1080pHDR, 1080p, 720p60, 720p (defaultSelect), 540p, 480p, 360p` with `{qualityType, qualityLabel,
definition, disableAutoSwitch?, limitType?, defaultSelect?}`. Individual videos expose only the
tiers they were transcoded to (the 5 above), so treat `qualityConfig` as the label dictionary and
`representation[]` as the actual streams.
### 4.2 m3u8 reachability — [VERIFIED]
```
m3u8 fetch HTTP 200 ; first line: #EXTM3U
```
- Signed query: `?sign=<md5>&t=<unix seconds>&us=<int>`. **Expiring** (the `t` value is a future
timestamp ≈ +? minutes; treat the URL as short-lived, re-call playInfo when it 403s).
- Streams are HLS (`#EXTM3U` with `.ts` slices; `m3u8Slice` also inlines the playlist in pageInfo).
- CDN hosts seen: `tx-video.acfun.cn`, `tx-safety-video.acfun.cn`, `ali-safety-video.acfun.cn`.
- Anonymous access is fine — no cookies needed for either playInfo or the CDN.
### 4.3 Danmaku (弹幕) read — [VERIFIED]
```
POST https://www.acfun.cn/rest/pc-direct/new-danmaku/list
Content-Type: application/x-www-form-urlencoded
resourceId={videoId}&resourceType=9&enableAdvanced=true&pcursor=1&count=200&sortType=1&asc=false
```
- `resourceId` = **`videoId`** (part id). `resourceType` = **9** (video).
**[VERIFIED]** `resourceId=42588725` (ac number) → `totalCount=0`; `resourceId=32224197`
(videoId) → `totalCount=5`, 5 items.
- Old-style XML danmaku endpoints are dead; this is the JSON API the current PC player uses.
**Evidence (HTTP 200):**
```json
{"result":0,"danmakus":[
{"likeCount":0,"color":16777215,"mode":1,"roleId":0,"danmakuId":287561494,
"danmakuType":0,"danmakuStyle":…,"danmakuAvatarUrl":…,"isLike":false,"rank":5,
"createTime":1778494836010,"userId":77697264,"body":"啊啊啊好萌的和声",
"danmakuImgUrl":…,"position":11880,"size":25}, …],
"styleDanmakuCount":0,"requestId":"…","pcursor":"no_more",
"host-name":"hb2-acfun-kce-node24.aliyun","totalCount":5}
```
| Plugin field | Path | Sample |
|---|---|---|
| danmaku id | `danmakuId` | `287561494` |
| text | `body` | `啊啊啊好萌的和声` |
| time offset in video | `position` (**milliseconds**) | `11880` |
| color | `color` (int RGB) | `16777215` (white) |
| mode (scroll/top/bottom) | `mode` | `1` (1 scroll, 5 top, 4 bottom per player conventions) |
| font size | `size` | `25` |
| created | `createTime` (ms epoch) | `1778494836010` |
| author | `userId` | `77697264` |
| advanced/style | `danmakuType`, `danmakuStyle`, `danmakuImgUrl`, `danmakuAvatarUrl` | — |
| likes | `likeCount`, `isLike` | `0`, `false` |
Notes:
- Page `danmakuCount` (8) ≠ `totalCount` here (5): some entries are filtered/special; also
`enableAdvanced=true`/`sortType=1` change the set. Use `totalCount` as authoritative for the API.
- Pagination: `pcursor` from the response (`"no_more"` when done), `count` page size, `asc` order.
- Posting danmaku (write, not executed): `POST /rest/pc-direct/new-danmaku/add` with
`body,color,id(=ac number),mode,position,size,subChannelId,subChannelName,type=douga,videoId`
— **[UNVERIFIED]**, requires login, `Referer` set to the video page.
---
## 5. Hot / ranking / feeds — mixed
### 5.1 Banana / channel ranking — [VERIFIED] (the only public "hot" list that works)
```
POST https://www.acfun.cn/rest/pc-direct/rank/channel
Content-Type: application/x-www-form-urlencoded
channelId=0&rankPeriod=day&rankLimit=10 # rankPeriod: day | week ; GET also works
```
**Evidence (HTTP 200):**
```json
{"result":0,"host-name":"hb2-acfun-kce-node24.aliyun","rankList":[
{"groupId":"Ml8wXzE3OTMwNzAzMDY4ODhfMzg1MA_1&-124&&rlg1","userId":17735808,
"dougaId":"48940670","contentId":48940670,"title":"XBOX联名,一个比一个抽象",
"userName":"Shi咪","channelName":"生活日常","channelId":86,
"viewCount":1615,"danmuCount":47,"commentCount":23,"bananaCount":1072,"likeCount":253,
"durationMillis":358099,"videoCover":"https://tx-free-imgs.acfun.cn/newUpload/…",
"createTimeMillis":1793014710758,"isFollowing":false,"isLike":false,"isFavorite":false,
"isThrowBanana":false,"tagList":[…],"videoList":[…],"user":{…},"channel":{…}}]}
```
`rankList[]` is a **superset of the search item shape** — it carries `title`, `description`,
`channelName`, `tagList`, `durationMillis` and full `user` object, so it is a decent
"browse without search" feed. Field names: `dougaId`/`contentId` (ac number), `contentTitle`
(alias of title on some nodes), `videoCover`/`coverUrl`, `createTimeMillis`, `viewCount`,
`danmuCount`, `commentCount`, `bananaCount`, `likeCount`, `durationMillis`.
`POST /rest/pc-direct/rank/banana` (`channelId=0&rankPeriod=DAY`) → **[VERIFIED]** HTTP 200,
same `rankList` shape (每日香蕉榜).
### 5.2 Following feed — [VERIFIED] route, empty without login
```
GET https://www.acfun.cn/rest/pc-direct/feed/webPush?count=20&pcursor=0
```
```json
{"result":0,"feedList":[],"followUpers":[],"pcursor":"no_more",
"host-name":"hb2-acfun-kce-node24.aliyun","requestId":"…"}
```
Route exists and returns a valid envelope, but `feedList` is empty anonymously → **login required
for real content** [UNVERIFIED for populated shape; item fields match the rank item shape per
community docs: `videoId`, `caption`, `coverUrl`, `playDuration`, `channel`, `viewCount`,
`commentCount`, `user`, `tag`, `likeCount`, `createTime`].
### 5.3 Dead endpoints — **[VERIFIED 404]** (all returned the 102.5 KB SPA shell)
```
GET/POST /rest/pc-direct/rank/video (tried rankId=1, channelId+rankPeriod variants)
GET/POST /rest/pc-direct/rank/video/day
/rest/pc-direct/rank/douga
/rest/pc-direct/rank/list
/rest/pc-direct/rank/hot
/rest/pc-direct/feed/recommend
/rest/pc-direct/feed/hot
/rest/pc-direct/feed/list
/rest/pc-direct/recommend/list
/rest/pc-direct/recommend
/rest/pc-direct/video/recommend
/rest/pc-direct/hotVideo
/rest/pc-direct/index/recommend
/rest/pc-direct/index/hot
```
There is **no working anonymous "recommended for you" JSON endpoint** as of 2026-10. The homepage
(`https://www.acfun.cn/`) is fully SSR (pagelets `pagelet_douga`, `pagelet_dance`, …,
`pagelet_monkey_recommend`); a plugin that wants a home feed must either scrape those pagelets from
the HTML or use `rank/channel`.
---
## 6. Interactions (cookie-authenticated) — routes **[VERIFIED]**, semantics **[UNVERIFIED]**
> **I never executed a successful write** (no AcFun account, and the task forbids logging in).
> Everything below is proven to exist by unauthenticated probing: each endpoint answers
> **HTTP 401 with JSON** instead of the 404 app shell. Parameters come from the site's own
> JS bundles (`@ac/pc-comment.*.js`, `newVideo/widget/bread/bread.*.js`) — that is the exact
> contract the browser uses.
### 6.1 Like / "顶" (点赞 + 取消) — [VERIFIED route, 401]
The PC web does **not** use a `pc-direct` thumb endpoint (all `/rest/pc-direct/thumb/*` → 404).
It calls the Kuaishou-side interact service:
```
POST https://kuaishouzt.com/rest/zt/interact/add # 点赞
POST https://kuaishouzt.com/rest/zt/interact/delete # 取消点赞
Content-Type: application/x-www-form-urlencoded; withCredentials: true
```
Form fields (from `bread.es6`, `likeParams` + token merge):
| Param | Value |
|---|---|
| `objectId` | **ac number** (`contentId` / `dougaId`) |
| `objectType` | `2` |
| `interactType` | `1` |
| `subBiz` | `mainApp` |
| `kpn` | `ACFUN_APP` |
| `kpf` | `PC_WEB` |
| `acfun.midground.api_st` | token from §7, param name is `` `${sid}_st` `` with `sid = "acfun.midground.api"` |
| `userId` | `userId` returned by the token call |
| `extParams` | object: `{isPlaying, showCount, otherBtnClickedCount, playBtnClickedCount}` (jQuery serializes as `extParams[...]`) |
Success response: `{"result":1,"hostName":"…"}` (`1`, not `0`). Button state then flips locally.
**Evidence (unauthenticated):**
```
POST https://kuaishouzt.com/rest/zt/interact/add
-> HTTP 401 {"result":-401,"error_msg":"token value error","error_id":""}
```
Note: pageInfo contains `window.likeDomain = "https://kuaishouzt.com"` — use that value rather than
hardcoding; it is how the page resolves the host.
### 6.2 Coin / 投蕉 — [VERIFIED route, 401]
```
POST https://www.acfun.cn/rest/pc-direct/banana/throwBanana
Content-Type: application/x-www-form-urlencoded
resourceId={ac number}&count={1..5}&resourceType=2
```
**Evidence (unauthenticated):**
```
POST /rest/pc-direct/banana/throwBanana (resourceId=42588725&count=2&resourceType=2)
-> HTTP 401 {"result":-401,"error_msg":"user not login"}
```
App/legacy duplicate (same error class): `POST https://api-ipv6.acfunchina.com/rest/app/banana/throwBanana`
with `resourceId,count,resourceType`. Success shape per community docs:
`{"result":0,"extData":{"bananaRealCount":5,"criticalHitInfo":null}}`. Rules: cannot banana your own
video (page checks `auth_key !== videoInfo.user.id`), 1–5 per call.
### 6.3 Favorite / 收藏 — [VERIFIED routes, 401 "token value error"]
Videos ("v" category, `resourceType = 9`) use a multi-folder system:
```
POST /rest/pc-direct/favorite/folder/list {resourceId}
POST /rest/pc-direct/favorite/folder/add {name} # 1-40 chars, CJK/alnum/_
POST /rest/pc-direct/favorite/resource/add {resourceId, resourceType, addFolderIds} # comma list
POST /rest/pc-direct/favorite/resource/remove {resourceId, resourceType, delFolderIds}
POST /rest/pc-direct/favorite/resource/updateFolder {resourceId, resourceType, addFolderIds, delFolderIds}
```
`resourceId` = **ac number**, `resourceType` = **9** for videos. Max 10 folders per operation; a
folder can be full (`resourceCount >= favoriteCountLimit`) or `type:1` (default folder, no limit).
Bangumi (番剧) uses a different pair: `POST /rest/pc-direct/favorite` `{resourceId,resourceType}`
and `POST /rest/pc-direct/unFavorite` `{resourceIds,resourceType}`.
**Evidence (unauthenticated):**
```
POST /rest/pc-direct/favorite/resource/add -> HTTP 401 {"result":-401,"error_msg":"token value error"}
POST /rest/pc-direct/favorite/folder/list -> HTTP 401 {"result":-401,"error_msg":"token value error"}
```
Important: favorites require the **midground token** (§7), not just the session cookie — the error
message differs from the plain `user not login` routes. Success is `result:0` plus a `dataList` for
`folder/list` (`[{folderId, name, type, resourceCount, favoriteCountLimit, inFolder}]`).
### 6.4 Follow / 关注 (and unfollow) — [VERIFIED route, 401]
```
POST https://www.acfun.cn/rest/pc-direct/relation/follow
Content-Type: application/x-www-form-urlencoded
toUserId={userId}&action=1&groupId=-1 # action: 1 = follow, 2 = unfollow ; groupId -1 = default
```
Group management (optional): `POST /rest/pc-direct/relation/getGroups`, `getFollows`
(`action=7` follows / `8` fans, `page`, `count`, `groupId`).
**Evidence (unauthenticated):**
```
POST /rest/pc-direct/relation/follow (toUserId=52326&action=1&groupId=-1)
-> HTTP 401 {"result":-401,"error_msg":"user not login"}
```
### 6.5 Post a comment (and delete/like) — [VERIFIED routes, 401]
```
POST https://www.acfun.cn/rest/pc-direct/comment/add
Content-Type: application/x-www-form-urlencoded
objectId={ac number}&content={UBB text}&replyToCommentId=0&subBiz=jimu&kpf=PC_WEB&kpn=ACFUN_APP
```
- `objectId` = ac number (`dougaId`), **not** `sourceId`/`sourceType` (despite the list endpoint
using those). This is what `@ac/pc-comment`'s `Ke()` sends.
- `replyToCommentId = 0` for a top-level comment, or the parent `cid` to reply.
- `content` is UBB text; emoticons are `[emot=acfun,{emotionId}/]`. `ztExtParams` is optional
(image payload for the image-comment mode).
- Success: `{"result":0,"commentId":…}` (the widget then reloads the list).
- A captcha/verification branch exists for `result` 400001–410999: the site redirects to
`error_url` and obtains a `{type, token}` before retrying — a plugin should surface that failure
rather than retry blindly.
Related:
```
POST /rest/pc-direct/comment/delete {sourceId, sourceType, commentId}
POST /rest/pc-direct/comment/like {commentId} # or {sourceId, sourceType, commentId}
POST /rest/pc-direct/comment/unlike {commentId}
```
**Evidence (unauthenticated):**
```
POST /rest/pc-direct/comment/add (objectId=42588725&content=test&replyToCommentId=0)
-> HTTP 401 {"result":-401,"error_msg":"user not login"}
POST /rest/pc-direct/comment/like -> HTTP 401 {"result":-401,"error_msg":"user not login"}
```
### 6.6 Miscellaneous authenticated read routes (useful for "am I logged in?")
| Route | Method | Anon result |
|---|---|---|
| `/rest/pc-direct/user/personalInfo` | POST | `401 {"result":-401,"error_msg":"user not login"}` [VERIFIED] |
| `/rest/pc-direct/user/userInfo?userId=` | POST/GET | public profile [UNVERIFIED live, route from bundle] |
| `/rest/pc-direct/relation/getGroups` | POST | 401 |
| `/rest/pc-direct/browse/history/list` | POST | 401 |
| `/rest/pc-direct/user/signIn` | POST | 401 |
`personalInfo` doubles as the cheapest session check: `result:0` + `info.userId` means the
`acPasstoken`/`auth_key` cookie pair is valid.
---
## 7. Token flow for like / favorite (midground token) — [VERIFIED endpoints, auth required]
The site's `common.es6` exposes `getTokenGen("acfun.midground.api")`:
```
POST https://id.app.acfun.cn/rest/web/token/get
Content-Type: application/x-www-form-urlencoded
sid=acfun.midground.api
```
Success (community docs, needs a logged-in cookie jar on `id.app.acfun.cn`):
```json
{"result":0,"ssecurity":"***","userId":625724,
"acfun.midground.api_st":"***","acfun.midground.api.at":"***"}
```
- `acfun.midground.api_st` is then sent as a **form field** (not header) to
`kuaishouzt.com/rest/zt/interact/*` and to `/rest/pc-direct/favorite/*`, with `userId`.
- The PC page itself calls this token endpoint through `common/static/js/common.*.js`
(`rest/web/token/get` appears there).
- **[VERIFIED]** unauthenticated: `POST https://id.app.acfun.cn/rest/web/token/get` →
`HTTP 401 {"result":-401,"error_msg":"token value error"}` — route exists, session required.
- Token is short-lived; re-fetch per action batch rather than caching for hours. Treat the whole
like/favorite flow as **[UNVERIFIED]** end-to-end because it cannot be tested without credentials.
---
## 8. CORS, redirects, rate-limit, encoding quirks
### 8.1 CORS — [VERIFIED]
Response headers on `/rest/pc-direct/search/video`:
```
HTTP/1.1 200 OK
Server: Tengine
Content-Type: application/json;charset=UTF-8
Access-Control-Allow-Origin: https://www.acfun.cn
Access-Control-Allow-Credentials: true
Strict-Transport-Security: max-age=172800
X-Cache: MISS TCP_MISS
```
- ACAO is pinned to `https://www.acfun.cn` — a browser page on another origin cannot read these
responses. **Node.js `fetch` is unaffected** (no CORS enforcement), which is why this plugin
approach works server-side.
- Cookie-authenticated calls need the cookie jar and `credentials: 'include'` only if you ever run
in a browser-like context; in Node just set the `Cookie` header.
- The kuaishouzt interact host answers with the same auth envelope; no ACAO needed from Node.
### 8.2 Required headers / redirects — [VERIFIED]
- No `Referer` and no custom UA needed for JSON reads (both tested → 200).
- `www.acfun.cn` serves everything over HTTPS directly; no redirect chains observed
(`curl -L` not needed). `m.acfun.cn` is a different (mobile) frontend — do not mix.
- 404s are **HTTP 404 with the full SPA HTML shell**, ~102.5 KB. A plugin MUST check
`content-type` / body prefix before `JSON.parse`; otherwise a dead path throws a confusing
JSON syntax error.
### 8.3 Rate limiting — [VERIFIED: none hit]
- 6 rapid consecutive search calls: `200 200 200 200 200 200`.
- During a 20-call batch at ~0 ms spacing I saw a few transient `curl` transport failures
(no response / connection reset), which recovered immediately on retry. That is the only
throttling-like behavior observed.
- Practical advice: 200–500 ms between calls, retry once on network error; no `429` seen.
### 8.4 Encoding
- All JSON is UTF-8 (`Content-Type: application/json;charset=UTF-8`). Decode as UTF-8 — Chinese
text is not escaped, so a wrong code page garbles it (this bit me in the Windows console only).
- `Content-Type` for POSTs: `application/x-www-form-urlencoded; charset=UTF-8`. json bodies are
**not** used by `/rest/pc-direct/*`.
- `playDuration` in search results is a `"mm:ss"` **string**, not seconds; `durationMillis`
elsewhere is milliseconds.
---
## 9. Recommended plugin mapping (what to call for each capability)
| Capability | Call |
|---|---|
| resolve `ac123` → detail | `GET /v/ac{acId}` → parse `window.pageInfo` (§1) |
| get part id for playback | `pageInfo.currentVideoId` (or search `videoId`) |
| search videos | `GET /rest/pc-direct/search/video?keyword=&sortType=0\|2` (first 30 only) |
| comments | `GET /rest/pc-direct/comment/listByFloor?sourceId={acId}&sourceType=3&page=N` |
| replies | `GET /rest/pc-direct/comment/sublist?sourceId={acId}&sourceType=3&rootCommentId={cid}&page=N` |
| play URL / qualities | `GET /rest/pc-direct/play/playInfo/ksPlayJson?videoId={videoId}` → `JSON.parse(playInfo.ksPlayJson)` |
| danmaku | `POST /rest/pc-direct/new-danmaku/list` (`resourceId={videoId}&resourceType=9&…`) |
| hot/ranking | `POST /rest/pc-direct/rank/channel` (`channelId=0&rankPeriod=day\|week&rankLimit=N`) |
| follow feed | `GET /rest/pc-direct/feed/webPush` (login) |
| like / favorite / follow / banana / comment | §6 + token §7 (login required) |
### Dead / do-not-use as of 2026-10-27
`/rest/pc-direct/video/{index,detail,getVideoInfo,info}`, `/rest/pc-direct/rank/video*`,
`/rest/pc-direct/feed/{recommend,hot,list}`, `/rest/pc-direct/recommend*`, `/rest/pc-direct/hot*`,
`/rest/pc-direct/thumb/*`, `/rest/pc-direct/coin/*`, `/rest/pc-direct/stow|collect/*`,
`/rest/pc-direct/comment/{list (POST anon), subCommentList, hotList, replyList}`,
`/rest/pc-direct/play/{playInfo,m3u8}`. All answered **404 + SPA shell** (or `result:11/21` for
routes that exist but reject anonymous/legacy usage). The current replacements are the SSR page
(§1), `play/playInfo/ksPlayJson` (§4) and `comment/listByFloor` (§3).
### Open items (explicitly UNVERIFIED — no login used)
1. End-to-end success shapes for like/favorite/follow/banana/comment-add (only 401 proofs here).
2. The exact minimum cookie set for authenticated calls (`acPasstoken`+`auth_key` per community
docs; PC additionally sends `_userid`, `acPlatform=2`, `csrfToken`, `_did`).
3. Whether `kuaishouzt.com` vs `api.kuaishouzt.com` matters for `/rest/zt/interact/*` in 2026
(page uses `window.likeDomain` = `https://kuaishouzt.com`; the older `api.` host returned 502 on
a probe of a different path).
4. Populated `feed/webPush` item shape (empty without login).