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.
This commit is contained in:
tamakyi
2026-10-27 11:26:51 +08:00
commit f9bedde19e
19 changed files with 3530 additions and 0 deletions
+841
View File
@@ -0,0 +1,841 @@
# 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).