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

38 KiB
Raw Permalink Blame History

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):

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

{"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:
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):

$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:

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):

{"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):

{"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):

{"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):

{"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):

{"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
{"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.


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):

{"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.

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).