跳转到内容

调试台与工具

控制台里有三个页面,可以让你在使用中把这套系统摸清楚:/playground 用表单驱动任意读取接口,并在失败时告诉你问题出在谁身上;/tools 把服务每次请求都要用到的几块能力直接暴露出来;/docs 则是由你正在运行的这个实例自己生成的 API 参考。读完这一页,你应当能发起一次调用、在失败时读懂失败、把那次上游请求原样在本实例之外复现出来,并把一个签名拆开来看。

页面 路径 做什么 代价
调试台 /playground 用表单调用真实的读取接口,展示归一化结果、平台原始响应、失败要素以及可直接复制的代码片段 一次真实的上游请求和一个身份,除非命中缓存
基础工具 /tools 计算签名、反解签名、解析链接、批量解析、铸造游客身份 除铸造外全部为零;铸造要启动一个浏览器
接口文档 /docs 用控制台当前语言渲染本实例自己的 OpenAPI 文档

它们之所以分开,是因为代价不同。/tools 上除最后一个标签页外,全都是对你输入的字符串做算术:不发网络请求、不消耗身份、也不会在请求日志里留下一行。而 /playground 上的每一次操作都会经过调度器并真实消耗资源。把便宜的东西放在不会意外变贵的地方,正是这种拆分的意义。

三个页面都在登录之后才能访问。外壳、导航与角色见控制台总览


页面铺满整个视口,分为三栏可拖拽面板,而不是一列不断向下滚的卡片:

  1. 接口 —— 接口目录,下方是本次会话的调用历史。
  2. 请求 —— 当前所选接口的表单。
  3. 响应 —— 状态、载荷、失败要素、签名、代码片段。

分隔条的位置按浏览器记忆,存放在 dtk.split.playground 下。默认宽度为 16 / 42 / 42 百分比,且三栏分别不会窄于 170 / 320 / 320 像素。

三栏上方是一条地址栏,实时显示即将调用的方法与 URL(含查询串),随你输入而变化。发起请求按钮就在那里,紧挨着它要调用的 URL。在表单里按回车同样会发送。

响应面板默认折叠,需要时再展开——否则一份很大的原始响应会把它下面的所有内容顶出屏幕。唯一的例外是失败面板,它永远不折叠:调用失败时,它就是你来这个页面的全部理由。

共十项。第一项与平台无关;其余九项在表单顶部带一个平台选择器,路径形如 /api/v1/{platform}/...

条目 方法 路径 调度器接口名
解析任意链接 POST /api/v1/parse 展开链接后才能确定
作品详情 GET /api/v1/{platform}/video {platform}.content_detail
作品评论 GET /api/v1/{platform}/video/comments {platform}.comments
评论回复 GET /api/v1/{platform}/video/comments/replies {platform}.comment_replies
作者资料 GET /api/v1/{platform}/user {platform}.author_profile
作者作品列表 GET /api/v1/{platform}/user/posts {platform}.author_posts
作者点赞列表 GET /api/v1/{platform}/user/likes {platform}.author_likes
合辑 / 播放列表作品 GET /api/v1/{platform}/mix/posts {platform}.mix_posts
作者粉丝列表 GET /api/v1/{platform}/user/followers {platform}.author_followers
作者关注列表 GET /api/v1/{platform}/user/following {platform}.author_following

最后两项仅 TikTok 提供。注册表里只有 tiktok.author_followerstiktok.author_following,没有对应的抖音条目,因此在平台选择器停在抖音时点这两项,会在 API 边界就被以 UNSUPPORTED_CONTENT(400)拒掉:没有 request_id,失败面板也匹配不到同名的调度器接口。这种不对称来自平台本身——即使拿一个健康的游客身份去问,抖音对关注关系也只回“未登录”;而一个照样把任务提交下去的路由,只会返回一页空数据,让你以为这位作者没有粉丝。调试台默认停在抖音,所以如果你先点了这两项,看到的就是它。

调度器接口名是令牌桶与熔断器的键,也是失败面板和健康度表格所使用的名字。POST /api/v1/parse 没有固定的接口名,因为 worker 只有在跟随链接之后才知道自己要调哪个接口。

这份目录是手工维护的读取接口清单,并不是 OpenAPI 文档的渲染结果——API 的其余部分(身份、下载、设置、资料库)不在这里。那些请用 /docsREST API 指南

字段分为三组。

目标(Target)。 你要问的对象。两个字段互为备选时——urlaweme_idurlsec_user_id——必须且只需填其中一个;两个都空时,表单会在两个字段上同时提示。有些接口则是真正的必填项:回复接口的 comment_id、合辑接口的 mix_id

分页(Paging)。 支持分页的接口上会出现 cursorcountcount 在表单和 API 两侧都被限制在 50 以内。

请求(Request)。 每个读取接口都接受的信封参数:

参数 类型 控制台默认值 作用
include_raw boolean 同时返回平台自身未经处理的响应。API 自身的默认值是关;控制台把它打开,是因为对照两者正是这个页面存在的意义。它很大——单个作品动辄数百 KB。
wait number 在同一连接上等待任务这么多秒。留空或填 0 会立即返回 task_id。表单允许 0–30;实例上限由 api.max_wait_seconds(默认 30)决定,超过就是 400。
proxy text 让上游请求走这个出口。除非管理员把 security.request_proxy 设为 publicany,否则会被拒绝。
identity 选择器 任意 只用指定的这一个身份发送,不会换成别的。需要 identity:manage(或 admin)以及 operator 角色。
refresh boolean 忽略响应缓存和正在进行的相同任务,重新向上游请求一次。
explain boolean 报告本次请求实际是怎么发出去的。见下文。

identity 做成选择器而不是输入框,是因为它的值是没人会凭记忆敲出来的 UUID,而你真正要选的其实是“用我的哪个账号”。列表里显示完整 id、身份状态,以及它是否已登录。已退休的身份不在列表中——退休会抹掉 cookie,所以它已经没有任何东西可以拿来发送了;在平台相关的接口上,也只会列出该平台的身份。切换平台时会清掉已经失配的绑定,而不是让你把抖音身份发到 TikTok 接口上,再去读那个 400。

如果选择器是空的,说明当前调用方无权列出身份。这在本页面上不是 bug:列出身份和使用身份受同一个 scope 管控。

即使实例的 security.request_proxy 是默认的 denyproxy 字段依然会显示。这是有意为之——一个明确指出设置项名称的拒绝,比“这个字段根本不存在”更有用。参见配置参考

在动用 refresh 之前,值得先弄清它意味着什么。有两套机制让重复调用变得免费:任务层会合并到一个正在进行或刚刚完成的相同任务上,取数层会缓存整形之后的响应体。refresh 把两者都关掉,代价是一个身份和一次真实的上游请求。缓存的存活时间取决于请求的对象,且可配置:

设置项 默认值 适用于
cache.content_ttl 1800 秒(30 分钟) 单个作品
cache.author_ttl 900 秒(15 分钟) 作者主页
cache.list_ttl 300 秒(5 分钟) 所有分页接口

校验优先在前端完成:必填项、二选一分组、数字与取值范围都会在发送之前检查,所以打错字不会有任何代价。

控制台使用你的会话 Cookie 调用。这个 API 以异步为先:不带 wait 的调用会返回 202 和一个 task_id,控制台会替你轮询。HTTP 请求 60 秒超时,任务轮询 180 秒后放弃。请求进行中会出现取消按钮。

结果落定后,响应栏顶部的状态条会给出:

含义
HTTP <status> 最终 HTTP 响应的状态码
耗时 浏览器侧测量,从点击到拿到结果——包含轮询时间
服务端耗时 meta.duration_ms,worker 记录的上游调用耗时
缓存命中 结果是否来自响应缓存(仅成功时显示)
request_id 关联 id,可复制。在日志页上就是用它检索

两个耗时分开显示是有意的。浏览器耗时长而服务端耗时短,说明时间花在了排队上,而不是花在平台上。

成功时有四个可折叠面板:

  • 归一化结果 —— 剥掉所有 raw 片段之后的载荷。这是所有调用方看到的稳定契约。
  • 原始响应 —— 平台自己的响应,只有设置了 include_raw 时才存在。没有时面板会说明,并提供带上原始响应重新请求按钮,一次点击即设置该参数并重新发送。
  • 响应元数据 —— 完整的 meta 对象。如果载荷是一页数据、带有游标且 has_more 不为 false,这里会出现获取下一页按钮;它会填好 cursor 并再次发送,也就是再消耗一次请求。
  • 可直接复制的调用示例 —— 见可直接复制的代码片段

失败面板会自动展开,把五样东西集中在一处:稳定错误码(若错误带有重试间隔则一并显示)、给人看的错误信息及提示、承载这次调用的身份、该接口的熔断状态,以及身份池现状。request_id 就在上方的状态条里。

这些正是区分平台改了我们没有身份了所需要的事实:

你看到的 通常意味着
有错误码、有身份、熔断为 closed、身份池健康 问题出在这一次请求或这个目标上。去读错误码。
该接口熔断为 open 连续失败触发了熔断,这个接口正在被休息。所有接口的熔断状态都列在总览页(/)上,见控制台总览;熔断打开期间该怎么办见身份与代理
身份池 可用 0 没有任何身份能承载请求。在有身份恢复之前,调用会以 IDENTITY_POOL_EXHAUSTED 被拒。见身份与代理
完全没有 request_id 请求根本没进入调度器——它在 API 边界就被拒了(scope、参数校验、限流)。

承载身份有两个来源。如果这次运行要求了 explain,并且控制台走的是异步路径——没填 wait,或者 wait 到期了——身份会随任务一起立刻返回并显示。否则控制台会拿 request_id 去请求日志里查,而日志会略有延迟;延迟期间面板会如实说明并提供刷新按钮,而不是假装答案不存在。

即使开了 explain,只要表单里填了 wait,走的也是后一种情况。控制台只从它轮询到的那个任务里读取 explain,所以在 wait 窗口内直接同步返回的结果,身份仍然要靠请求日志去查,而签名、cookie 与上游 curl 三个面板则会一直是空的。打算解释的那些调用,请把 wait 留空。

响应下方还有一个接口与身份池面板,即使你一次调用都还没发起也可以看:所选接口的熔断状态(或者全部接口中有多少个已熔断的计数)、成功率、风险率、样本数,以及按状态统计的身份池分布。它每隔数秒轮询一次,因此请求失败的那一刻看到的就是当时的状态。一个赶来排查“为什么什么都不好使”的运维,需要的正是这些,而不是先看响应体。

签名面板列出本次请求实际携带的签名参数,直接从 worker 发出的那个 URL 里读出来。值完整展示,不做省略——看签名的目的就是拿它跟别的东西比对,而半个签名跟什么都比不了。

它会挑出这些参数:

a_bogusX-BogusX-GnarlyX-DynosaurmsTokenx-secsdk-web-signatureverifyFpfpuifid

反解这些签名按钮会把整条已签名 URL 连同它的 User-Agent 一起提交给 /api/v1/tools/decode,并就地渲染结果——和反解签名标签页看到的是同一套视图。这是单独的、需要你主动点的一步,而不是自动发生的:大多数调用并不会问这个问题,自动触发就等于为每次调用都多花一个来回。

这个面板需要有一份 explain 才能有内容,因为真正起作用的签名是 worker 针对平台自己的 URL 生成的,而那个 URL 并不属于普通响应的一部分。没有 explain 时,面板会如实说明,并提供重新发起并说明本次请求

它还需要异步路径。控制台是从它轮询到的那个任务里取出 explain 的,所以一次填了 wait、并在窗口内就完成的调用,面板照样显示“已开启 explain。发起请求后,此面板就会填上内容。”的提示——尽管 explain 确实生效了、也被审计了;它下方的 cookie 与上游 curl 空着也是同一个原因。把 wait 清空再发一次即可。

explain —— 本实例究竟发出了什么请求?

Section titled “explain —— 本实例究竟发出了什么请求?”

explain=true 让实例把它发出的那次上游调用按原样描述出来。

返回什么

字段 含义
method 向上游发送时使用的方法
url 完整的平台 URL,含签名参数
headers 传输层即将发送的全部请求头——签名器贡献的那些,加上该身份自己的 User-Agent、语言与客户端提示
cookie_header 该身份的 cookie,拼成一个可直接粘贴的 Cookie: 请求头
identity_id 承载这次尝试的身份
signer 生成签名的签名器
endpoint 内部接口名,例如 douyin.content_detail
proxy 该身份的出口,已脱敏

代价。 explain 描述的是一次真实尝试,而命中缓存的响应并没有发起任何尝试——因此 explain 隐含 refresh:它会跳过响应缓存,而且带 explain 的调用永远不会被合并到一个不带 explain 的在途任务上。每一次带 explain 的调用都是一次真实的上游请求,都会消耗一个身份。

谁可以要。 返回值里含有一份活的 cookie,因此它按身份池管理而不是按读取来管控:需要 identity:manage(或 admin)scope 具备 operator 角色。一个 douyin:read 的 key 可以请求本实例去使用某份 cookie;但它不能要求本实例把那份 cookie 交给它,而“把你发出的请求给我看”只是绕了个远路的同一件事。

它会被审计。 每一次被接受的 explain 都会向审计日志写入一条 request.explained,记录是谁、针对哪个接口提的要求——绝不记录 cookie 本身。这与身份页展示 cookie 时所做的取舍是同一笔交易。

它在失败时依然可用。 explain 在成功和失败两种结果上都会被保存,因为“请求被拒了,让我看看我们发了什么”正是这个功能存在的理由。失败的任务和成功的任务把它放在完全相同的位置。

对无权查看者会被剥离。 explain 与任务结果一起保存。任何人回读这个任务时,除非该读者本来就有权直接查看那份 cookie,否则 explain 块会被移除。

它在协议上出现在哪。 在响应元数据的 explain 键下——查询任务时是 result_meta.explain;而当你给的 wait 足够长、任务成功的结果直接同步返回时,它在 meta 里。若任务是在 wait 窗口内失败的,返回的就是一个错误信封,而错误信封的 meta 里只有 request_id。explain 依然存在那个任务上,只是必须回头用 GET /api/v1/tasks/{task_id} 去读它的 result_meta.explain

有了 explain,接口与身份池面板就会多出关于眼前这次运行的具体事实:身份 id、签名器、脱敏后的出口,以及那份 cookie。

cookie 藏在显示完整 cookie这一次点击之后,而不是藏在掩码之后。半份 cookie 帮不了任何人,而你已经指名要它了;这一次点击换来的,只是它不会就那么摊在一块共享屏幕上。点击之前,面板只显示一共有多少条 cookie。

把你复制到的东西当作凭证对待。它是某个真实平台账号或游客的活会话,任何拿到它的人都能用,它不该出现在 bug 报告、共享的 Postman 工作区或聊天消息里。参见安全

cookie 下方,控制台会把整个上游请求拼成一行 curl:平台自己的接口、逐字节原样的已签名查询串、传输层原本要发的全部请求头,以及该身份自己的 cookie。

这正是任何人排查一个被拒的调用时最终都会去手工拼的东西,而手工拼恰恰是出错的地方。有三件事必须对齐,否则平台就会拒绝你;而这三件事在这里都是已知的,在此之前则无从猜起:

  1. 查询串必须逐字节原样发送。 签名覆盖的是它自己的编码器产出的那个查询串。重新编码它——大多数 HTTP 客户端都会热心且无声地这么做——就改变了签名所覆盖的字节。
  2. User-Agent 必须是参与哈希的那一个。 两个平台都会把 UA 计入签名。“我发的 UA 不是我签名时用的那个”,是手工构造请求时看起来完全正确却失败的最常见原因。
  3. cookie 必须是该身份自己的。 平台响应的是一个会话。签名正确但不带 cookie,得到的是 200 和一个空响应体。

这行 curl 的有效期也很短。签名里带着时钟,cookie 里带着会话,两者都会过期。如果它十分钟前还能用而现在不行了,重新发起一次带 explain 的调用,而不是去调试那行旧命令。

Postman 的导入器接受原始 cURL 命令——Import → Raw text,粘贴,它就变成一个请求。有两个注意点在这里比在多数 API 上都重要得多:

  • 确认查询串没被改动。 任何重新编码查询串的客户端都会破坏签名,而失败的表象是“签名不对”,而不是“客户端不对”。导入之后,把 Postman 里的 URL 和你粘贴的那一行逐字符比对。如果不一致,就把整条 URL 当作一个不透明字符串发送,而不要让它被解析成键值对参数。
  • TikTok 无论如何都会拒绝它。 TikTok 会校验 TLS 指纹与 User-Agent 是否自洽。Postman、curlhttpx 以及所有普通 HTTP 客户端都会呈现它们自己的 TLS 指纹,因此一个 Chrome 的 User-Agent 走在非 Chrome 的握手上,无论签名多正确都会被拒。这不是加个请求头能解决的问题——本项目之所以自带浏览器级传输层,原因就在这里。在抖音上,同一行命令通常是可以跑通的。

同样的提醒也适用于 curl 本身:在一台 curl 并非基于浏览器 TLS 配置编译的机器上,用它来确认发出去的是什么没问题;但不要因为它失败就断定签名有错。

可直接复制的调用示例面板会按表单当前的状态生成代码片段,共三种:

片段 生成内容
curl curl -X <method>,带 Authorization: BearerAccept-Language;POST 时另加 Content-Type-d
Python 一段 httpx 调用,含 paramsjson、请求头与 60 秒超时,最后打印 payload["data"]
JavaScript 一段 fetch 调用,在 payload.success === false 时抛错,否则输出 payload.data

每个片段里都带着占位符 key dtk_xxxxxxxx_your_key_here。请把它换成 API 密钥页面上真实的 key——控制台自己用的是会话 Cookie,而脚本没有。在浏览器之外,请使用 Authorization: Bearer <key>X-API-Key 请求头。

这些片段调用的是本 API。它们不是上游 curl,也不需要任何 cookie。

接口目录下方是本次会话最近 12 次调用:调了哪个接口、哪个平台、成功与否、耗时多少。点击其中一条会把它的参数填回表单——并且刻意不会重新发送,因为一次会重放上游调用的点击,等于在你没打算的情况下消耗掉一个身份。

历史只保存在内存里,离开页面即消失。一次请求可能携带代理密码或身份 id,这两样都不该存进比标签页活得更久的地方。


五个标签页,背后是五个 API 接口。这些是服务每次请求都要用到的零件,之所以公开出来,是因为这是一个开源项目,会有人在它们之上做东西。除最后一个标签页外,这里的一切都是对你输入内容的纯函数运算:不发网络请求、不消耗身份、不产生请求日志。

有一条提醒值得先说,因为它是所有人第一个撞上的:光有签名是不够的。 两个平台响应的都是一个会话,所以一个正确的签名在不带 cookie 的情况下发出去,得到的是 200 和一个空响应体。签名标签页会告诉你还有什么必须对齐;铸造标签页则是产出那样东西的地方。

POST /api/v1/tools/sign —— 为平台 API 地址计算所需的签名参数。纯算术运算:不发起任何请求,不消耗身份,结果只取决于你提交的内容。

字段 长度上限 说明
平台 douyintiktok
API 地址 4096 字符 含查询串的完整 API 地址,必须以 http://https:// 开头
User-Agent 512 字符 两个平台都会把它计入签名。留空则使用默认 Chrome UA,并在响应中回显
msToken 512 字符 仅 TikTok。它会被封进签名,因此必须是请求实际会携带的那个 token——填个假的比不填更糟
Cookie 8192 字符 任意粘贴格式:Cookie: 请求头、DevTools JSON、Netscape 格式皆可。抖音的 x-secsdk-web-signature 是对其中的访客 ID 计算的

留空时使用的默认 User-Agent:

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36

从示例开始。 两个按钮会用真实浏览器请求填好所有字段——抖音的主页查询和 TikTok 的作品列表。有两样东西被刻意去掉了,而这两处删除本身就是要点:签名参数没有了,因为它们正是这个工具算出来的,粘贴一个过期的签名是拿到“200 + 空响应体”的头号原因;cookie 的是占位符,因为一份 cookie 就是一个活的登录态。cookie 的名字是真的,而那正是人们看着这个字段时真正想知道的东西。

结果。

  • 签名后的地址 —— 可直接发送,并附带反解这个签名按钮,把整条 URL 交给反解标签页。这两个表单互为逆运算,把一个直接喂给另一个是你能用它们做的最有用的事。
  • 这个查询串是怎么拼出来的 —— 同一个结果的展开视图,流水线的每一层各占一格,按运行顺序排列。请求被拒时值得读的正是这部分,因为它告诉你缺的是哪一层,而不是丢给你一整条扁平的查询串。
  • 新增的参数 —— 签名参数,按算法名归类。
  • 签名所需的请求头 —— 这些必须与签名后的查询串一起发送。缺少它们时,抖音会拒绝受签名保护的接口。
  • User-Agent —— 回显。请用这个字符串原样发送请求。

各层:

贡献了什么
business 你放在 URL 里的参数,但不含 msToken——无论它写在哪里,它都是会话值,会被算进下一层
session msToken,以及说明其来源的 ms_token_sourceurlcookiesgeneratedabsent
signature 签名本体。seal_param 指出真正起签名作用的那个参数,roles 则逐参数说明同一件事
websign 仅抖音:对访客 ID 计算的 x-secsdk-web-signature,附带盐值与 md5 原文

上表里有两处不对称是平台自己造成的,不是本项目的选择,而且都属于读者容易去“修正”的那类东西:

  • 只有抖音会出现 generated URL 里没有 msToken 时,抖音的签名器会自己造一个。TikTok 的签名器从不这么做,因为 TikTok 会校验带上的 token,却接受不带 token——伪造的 token 会验证失败,缺失的 token 反而没事。没有 token 时,TikTok 的 session 层会以 no_ms_token_supplied 标记为未执行,并发送一个空的 msToken
  • 在 TikTok 上,algorithm 和真正的签名参数不是同一个。 algorithm 报的是 X-Bogus,因为那是这套方案在协议上的稳定名称;但 TikTok 网页端在 HTTP 请求上发的 X-Bogus 是常量 1X-Gnarly 才是覆盖查询串的签名,X-Dynosaur 是它所覆盖的环境上报。只按 algorithm 画出来的图,指向的恰好是那个什么都没签的参数,所以才有 seal_param 来指向正确的那个。

signature 层有两样东西不会展示给你,以及原因:

  • input_reconstructiblefalse,并且会一直是。签名是对其自身编码器产出的查询串计算的,而最终发送的字符串由另一个编码器构建,因此对某些输入而言,被签名的字节并不是被发送的字节。一个对大多数查询都正确的原文,会被当作算法文档来引用,那比不提供更糟。
  • websign 的原文展示,但只在重新计算并与旁边的签名核对通过之后。无法验证的原文会被丢弃而不是发布,理由同上。

如果你提交的 cookie 里没有 uifidwebsign 层会出现并以 no_uifid_cookie 标记为未执行。这不是无关紧要的细节:它就是 headers 为空的原因,也是受签名保护的抖音接口会以 uifid 为由拒绝该请求的原因。请到身份标签页铸造一份。

无论工具返回什么,旁边那份清单依然成立。请用回显的那个 User-Agent 原样发送请求;使用与之自洽的 TLS 指纹;并且带上 cookie。本项目在 2026-09-08 用这个接口自己的输出实测过:三者齐备时,返回 2537 字节和预期的主页数据;完全不带 cookie 时,200 和一个空响应体。

POST /api/v1/tools/decode —— 与旁边那个标签页互为逆运算,也是本项目自己逆向这些算法而不是照搬别人移植版的理由:自己拥有的实现才讲得清楚。同样是纯算术运算。

粘贴一条完整的已签名 URL,其中每个签名参数都会按它实际封住的那段字符串被拆开来读。这是最有用的形式:只有从完整 URL 出发,才能确切知道被覆盖的字符串是什么,也只有那时校验才跑得起来。单个值,或者一对 name=value,同样可以。值的长度上限是 8192 字符。

即使答案看起来显而易见,也请把 User-Agent 字段填上。两个平台都会把 UA 计入签名,而这正是你弄清“正在发送的 UA 是不是签名时用的那个”的办法。

按什么参数解读默认保持自动,此时会按其他参数都不具备的特征来识别。形态不明确时可以自己指定:a_bogusX-BogusX-GnarlyX-DynosaurmsTokenx-secsdk-web-signatureverifyFp

三种截然不同的东西看起来都像同一种表格行,而把第二种当成第一种,正是这套设计要防止的错误。每个字段都会声明自己属于哪一种。

类型 界面显示 含义
plain 已还原 直接从载荷里原样读出。aidpage_id、版本号、计数器、随机数
time 时钟 一个时钟,同时给出原始数字与 ISO-8601 时刻
environment 环境上报 SDK 对它自认为所处页面的描述——屏幕尺寸、各种标志位、canvas 与 WebGL 哈希
digest 哈希 一个哈希,或哈希的某几个字节。输入不可还原;见校验部分
checksum 校验和 内部冗余,在这里被重新计算——这正是证明反解正确的东西
opaque 未解释 每次抓包里都存在,但含义尚未确定。如实命名,不做臆测

所以如实的总结是:可还原的值会原样返回,哈希是被校验而不是被倒推,而下发型的值底下根本没有明文。 msToken 和访客令牌由平台签发或随机生成;对它们,反解器报的是 not_computed,而不是报告“没能找出本来就不存在的东西”。

对于任何哈希,反解器做的是校验而不是猜测。给它一个候选值——一条带查询串的 URL、一个 User-Agent——每项校验会返回下列之一:

状态 含义
match 你给的候选值就是这个签名封住的输入。被覆盖的确切原文会展示出来,因为它复现出了旁边那个值
differs 不是。正是这一项把“我的请求被拒了”变成“我正在发送的签名是对另一个 URL 计算的”
not_supplied 你没有提供候选值。这不是失败,展示方式也刻意与 differs 不同

每项校验还会给出有多少位的证据,好让 “match” 不会比它应有的把握更自信:

参数 每项校验的位数 说明
a_bogus 24 每条链 3 个 SM3 字节,分别覆盖 query、body 与 User-Agent
X-Bogus 16 双重 md5 的两个字节
X-Dynosaur 32 覆盖 query、User-Agent 与 body 的 32 位哈希
X-Gnarly 128 query、body 与 User-Agent 的完整 md5
x-secsdk-web-signature 128 重建原文的完整 md5
参数 平台 返回内容
a_bogus 抖音 头部 magic、SDK 版本、两个时钟、aidpage_id、双周计数、浏览器信息串、环境与探测标志位、调用桶与陷阱位——外加三条摘要链
X-Bogus 抖音 常量前导、精确到秒的时间戳、canvas 常量、三组 16 位摘要以及一个内部校验和。会标注它已被 a_bogus 取代
X-Bogus = 1 TikTok 报告为一个什么都没签的常量,而不是格式错误——TikTok 网页端发的确实就是这个字面量
X-Gnarly TikTok 十六个字段:两个校验和、env 与 ub 码、query/body/User-Agent 的 md5、时间戳、随机数、SDK 与 SCM 版本、调用序号
X-Dynosaur TikTok 二十五个字段:各类版本号、计数器、页面标识、WebGL/canvas/设备哈希、时间戳、四个 32 位哈希
msToken 两者皆有 只有长度、padding 与字母表。它是被签发的,不是算出来的。长度还能判断它属于谁——一个抖音 token 放进 TikTok 请求里会短 20 个字符,在有人去数之前看起来就跟签名 bug 一模一样
x-secsdk-web-signature 抖音 摘要本身;当访客 ID 与时间戳与它同行时,还会给出盐值和它的 md5 所覆盖的确切原文
verifyFpfps_v_web_id 抖音 前缀、以 base36 毫秒表示的生成时间,以及随机尾部。生成时间远早于携带它的请求,正是一份躺在文件里很久的 cookie 的形态

结果下方的注记会说明验证了什么。checksum_verified 表示内部折叠校验和的每一项输入本身都是载荷中的字段,且全部对得上,因此整个布局都正确还原了。noise_not_recoverable 表示有些字节是每次调用随机抽取的、不携带任何含义——同一毫秒内生成的两个签名大部分字节都不同,含义却完全一样。

当一个值完全读不出来时,原因会说明为什么:malformed(并附上未通过的结构检查)、one_waynot_computedunknown_parameter

GET /api/v1/tools/parse-url —— 不发起任何请求,直接判断一条链接指向什么。它接受分享链接,也接受里面埋着链接的整段剪贴板文本——后者才是两个 App 实际放进剪贴板的东西。

它会报告平台、链接指向的资源类型、ID、存在时的 handle、规范化 URL、它是否是受支持的目标,以及是否需要展开。

具体拿到哪个 ID 因平台而异,因为接口本身就不同:抖音主页链接给出 sec_user_id,TikTok 的则给出 @handle,因为那才是 TikTok 用户详情接口接受的东西。两者都放在 resource_id 下返回。

短链(v.douyin.comvm.tiktok.com)必须实际跟随跳转才能解析,而跟随跳转是一次这个接口承诺不做的网络调用。它们会带着 needs_expansion: true 且没有 ID 返回;请改为提交到 POST /api/v1/parse——也就是调试台目录里的第一项。

POST /api/v1/tools/parse-batch —— 对粘贴进来的一列内容做同样的识别,因为“一次一条”并不符合人们手上真正有的东西:从表格里拷出来的一列,或者某次抓取的输出。

每行一项粘贴进去。空行和重复项会被丢弃。一行是“分享文案 + 里面夹着一条 URL”时,得到的是那条 URL,而不是四个碎片;一行里有多条 URL 时,会得到多条。

限制 数值
返回条目数 1000
接受的文本长度 524288 字符

如果识别出的去重条目多于返回条目,结果里会带上 truncated,页面会告诉你实际解析了多少条,而不是让你自己去数。

每一行会被归入下列之一:

类型 含义
link 已识别的链接,带平台、资源类型与 ID
short_link 已识别的链接,但要知道目标需要一次网络调用
content_id 一个能对上的裸作品 ID
bad_id 一串数字,但不可能是作品 ID
unknown 两者都不是

两个平台生成的作品 ID,其高 32 位就是签发时的 Unix 秒,所以表格里还会显示每个 ID 的签发时间——它是从 ID 自身读出来的,而不是请求来的。这个时间等于或略早于发布时间,绝不会晚于它。

这项检查无法区分“从未存在过的作品”和“已被删除的作品”:看起来合理的 ID 就是看起来合理。它能做的是拒掉那些根本不可能是 ID 的文本——而这部分正是值得免费做掉的,因为另一种做法是发起一次上游请求、消耗一个身份,去换回同一个结论。

复制这些 ID 会把所有已识别的 ID 逐行放进剪贴板,跳过 bad_id 行。

POST /api/v1/tools/identity —— 驱动真实浏览器访问平台,把平台下发的 cookie 交还给你。这是无法用计算得到的那一半:TikTok 的 msToken 和抖音的 UIFID_TEMP 是平台签发给一个真正加载过其页面的浏览器的。没有任何算法能产出它们,这也正是“光有签名不够”的原因。

它的代价,直说:

  • 数十秒。 需要启动浏览器、加载页面,并等待其脚本把 cookie 设置完毕。
  • identity:manage scope。 普通读取 key 不带这个权限。
  • 一个 browser-rpc 服务。 没有配置 DTK_BROWSER_RPC_URL 时,调用会失败并指出这个设置项。browser-rpc 是整个栈里最重的容器,属于可选的 compose profile;见安装与部署

可选的代理字段让你通过指定出口铸造,而且值得设置:在一个地址铸造、却从另一个地址使用的 cookie,正是两个平台都在找的那种不一致。它与其他所有代理参数一样,要过同一道 security.request_proxy 关卡——工具接口不能成为绕过该设置的通道。

不做任何存储。 cookie 返回给你之后即被遗忘。若要把它加入本实例自己的身份池,请使用身份页或 POST /api/v1/admin/identities/mint——见身份与代理

返回内容包括 cookie、铸造时所用的浏览器指纹(User-Agent、浏览器系列与主版本号、平台、屏幕、语言、时区),以及它被看到的出口地址。请整套一起使用。cookie 与另一个 User-Agent 搭配使用的身份,比没有身份更弱。

标签页 接口 所需 scope 网络 消耗身份 耗时
计算签名 POST /api/v1/tools/sign douyin:readtiktok:read 瞬时
反解签名 POST /api/v1/tools/decode douyin:readtiktok:read 瞬时
解析链接 GET /api/v1/tools/parse-url douyin:readtiktok:read 瞬时
批量解析 POST /api/v1/tools/parse-batch douyin:readtiktok:read 瞬时
铸造身份 POST /api/v1/tools/identity identity:manage 一次真实浏览器会话 交给你一份新铸造的 cookie 数十秒

这五个接口与本 API 上的其他接口一样,都受限流约束。


/docs 是控制台自己的参考页。它把 Swagger UI 渲染在控制台外壳内部,指向 /openapi.json?lang=<language>(语言与控制台当前设置一致),并用控制台自己的设计令牌重新配色,免得它看起来像硬拼上去的另一个产品。这层覆盖只涉及颜色、字体与圆角——控件本身的行为保持原样。

组件上方有一张卡片,展示文档地址(可复制)、它声明了多少个路径与操作,以及它是用哪种语言生成的。如果它显示文档里没有任何接口,那属于部署问题而不是数据为空:api 容器没有注册任何路由。

接口默认全部折叠、模型不展开、显示请求耗时,授权信息在刷新后保留。Try it out 处于启用状态,且组件会带上浏览器凭据,所以你在登录控制台的情况下从 /docs 发起的调用,是由你的会话 Cookie 鉴权的——不需要粘贴任何 key。这也意味着它们是真实调用:在平台接口上点一次 Try it out,消耗身份的方式与调试台完全相同。

Swagger UI 的静态资源从公共 CDN 加载(cdn.jsdelivr.net/npm/[email protected]),超时 15 秒。在离线部署或 CDN 被屏蔽的环境下加载会失败,这属于预期而非故障:页面会如实说明,并提供服务端渲染的页面、原始文档以及重试按钮,而不是留下一个空白框。

API 还提供自己的文档页面,与控制台互不依赖:

路径 是什么
/swagger 服务端渲染的 Swagger UI,启用 Try it out,所有分组默认折叠
/redoc ReDoc,只读的三栏式参考文档
/openapi.json 原始 OpenAPI 文档

这两个页面会把自己的 ?lang= 转发给 schema 地址,所以 /swagger?lang=zh 拿到的是中文文档。语言按这个顺序解析:?lang=Accept-Language、实例的 api.default_language。不受支持的 ?lang= 取值会被忽略而不是报错,解析继续走请求头。支持的取值是 enzh

控制台刻意占用 /swagger/docs 是控制台页面,需要会话;/swagger 保持免凭据,供没有控制台账号的调用方使用——对一个公开实例而言,那是绝大多数使用者。两者同时占用一个路径,会导致你看到哪个页面取决于你是怎么进来的:客户端跳转给你控制台,刷新给你裸文档——而展开一个分组之后,人们做的恰恰就是刷新。

由于 /swagger/redoc/openapi.json 不需要会话,任何能访问到你的实例的人都能读到它的接口面。对一个以“被别人的程序调用”为目的的服务来说,这是有意为之,但在你把端口暴露出去之前,这是一个需要权衡的事实。参见安全

文档声明了两种真实存在的鉴权方式,所以 Authorize 按钮是可用的:

方式 位置
X-API-Key 请求头 API 密钥页面上创建的 key
dtk_session Cookie 登录控制台后自动设置,浏览器会自动带上

安全声明是逐接口而不是全局的,因此公开接口不会被错误地标成需要 key。

文档只对响应信封——successdataerrormeta——做类型声明,而不声明每个接口 data 的具体形状。这是一个刻意的取舍:响应是以字典方式组装的,在 schema 里再声明一遍只会让两者逐渐分叉。生成式客户端真正需要的,是那部分永远不变的结构。


这些页面上的一切都是普通的 API 接口。先准备一个具备相应 scope 的 key——见用户与 API 密钥——如果你的实例不在默认的回环地址上,请相应调整主机地址。

终端窗口
export DTK_KEY='dtk_...your key...'
export DTK_URL='http://127.0.0.1:8000'

识别一条链接,不消耗任何东西:

终端窗口
curl -s -G "$DTK_URL/api/v1/tools/parse-url" \
--data-urlencode 'url=https://www.douyin.com/video/7300000000000000000' \
-H "X-API-Key: $DTK_KEY"

给粘贴进来的一列链接和 ID 分门别类:

终端窗口
curl -s -X POST "$DTK_URL/api/v1/tools/parse-batch" \
-H "X-API-Key: $DTK_KEY" \
-H 'Content-Type: application/json' \
-d '{"text":"7300000000000000000\nhttps://www.douyin.com/video/7300000000000000001"}'

为一个地址计算签名:

终端窗口
curl -s -X POST "$DTK_URL/api/v1/tools/sign" \
-H "X-API-Key: $DTK_KEY" \
-H 'Content-Type: application/json' \
-d '{"platform":"douyin","url":"https://www.douyin.com/aweme/v1/web/aweme/detail/?aid=6383&aweme_id=7300000000000000000"}'

反解一个签名,并用一个候选 User-Agent 与它比对:

终端窗口
curl -s -X POST "$DTK_URL/api/v1/tools/decode" \
-H "X-API-Key: $DTK_KEY" \
-H 'Content-Type: application/json' \
-d '{"value":"https://www.douyin.com/aweme/v1/web/aweme/detail/?aid=6383&a_bogus=...","user_agent":"Mozilla/5.0 ..."}'

铸造一个游客身份并自己留着——需要 identity:manage,耗时数十秒:

终端窗口
curl -s -X POST "$DTK_URL/api/v1/tools/identity" \
-H "X-API-Key: $DTK_KEY" \
-H 'Content-Type: application/json' \
-d '{"platform":"douyin"}'

让一个数据接口说明自己发出的请求——需要 identity:manage 与 operator 角色,会跳过缓存,并被审计:

终端窗口
curl -s -G "$DTK_URL/api/v1/douyin/video" \
--data-urlencode 'url=https://www.douyin.com/video/7300000000000000000' \
--data-urlencode 'wait=30' \
--data-urlencode 'explain=true' \
-H "X-API-Key: $DTK_KEY"

当调用在 wait 之内成功时,说明内容位于响应元数据的 explain 键下。其余所有情况都要回头去看任务:wait 不足以让任务完成时,你拿到的是一个 task_id;而任务在窗口内失败时,返回的是一个错误信封,它的 meta 里只有 request_id。这两种情况都请用 GET /api/v1/tasks/{task_id} 读取 result_meta.explain

完整的请求与响应契约见 REST API 指南。在终端里做同样的事,见命令行参考


  • 核心概念 —— 身份、租约、熔断、签名器到底是什么。如果失败面板里的词汇让你陌生,值得先读它。
  • 控制台总览 —— 日志页(拿 request_id 检索的地方),以及列出每个接口熔断状态的总览页。
  • 身份与代理 —— 调试台所取用的身份池从哪里来、池子空了怎么办,以及调度器页面和熔断打开时该做什么。
  • 运维 —— 备份、通知、保留策略与监控,以及容器日志里有、而日志页上没有的东西。
  • 故障排查 —— 针对这些页面会呈现给你的那些具体故障。
  • 安全 —— 一份被展示出来的 cookie 意味着什么,以及谁才应该有权索取它。