身份与代理
身份池是决定这套系统能不能跑起来的关键部分。读完本文,你将能够读懂身份列表和它的状态机、铸造或导入身份、把身份绑到代理上、理解消耗它们的调度器、按自己的流量规模估算池子大小,并且在「什么都不响应了」时分清三种完全不同的故障。
为什么需要身份池
Section titled “为什么需要身份池”这些平台都不发放 API Key。每个请求都必须看起来像一次普通的浏览器访问:带着平台自己下发的 Cookie、互相自洽的 User-Agent 与 TLS 指纹,以及这份 Cookie 曾经出现过的出口地址。
因此这套系统轮换的最小单位不是一条 Cookie,而是一个完整的身份:Cookie、浏览器指纹,以及(可选的)一个代理,三者绑定终身。在同一个出口、同一个 User-Agent 后面轮换 Cookie,其异常程度高于单纯的请求频率——在平台看来,那是一台设备在不停地换访客,而真实浏览器不会这么做。
由此引出两个后果,两者都是刻意的取舍,不要试图「修正」它们:
- 身份永远不会被重新配到另一个出口。 代理挂了,它后面的身份只会进入冷却,不会被搬走;代理被删除,它们会被退休。把一份还活着的 Cookie 重新配上一个新的出口 IP,是这套系统能做出的关联性最强的动作。
- 每个身份同时只跑一个请求。 单身份并发数是 1,而且应当保持为 1:真实会话本来就不会并发发 API 请求。提升吞吐靠增加身份,而不是调大这个数。
一个身份由什么组成
Section titled “一个身份由什么组成”| 字段 | 含义 |
|---|---|
cookies |
Cookie 罐,以 AES-GCM 密文存储,并与该行的 id 绑定。除了那条带审计的显示接口,任何 API 响应都不会包含它。 |
fingerprint |
browser_family、browser_major、user_agent、platform、screen、language、timezone,以及浏览器上报了的话还有 hardware_concurrency 和 device_memory。 |
proxy_id |
该身份铸造或导入时所处的出口。null 表示走服务器自身的出口。 |
authenticated |
导入时 Cookie 中带有登录态标记则为 true。这是关于那次粘贴的事实,不是关于账号的事实。 |
source |
minted(由本实例产生:抖音和 TikTok 由无头浏览器产生,B 站由一次普通 HTTP 请求产生)或 imported(你粘贴进来的)。 |
state |
active、cooling、degraded、retired——见下文。 |
consecutive_fails |
连续失败次数。一次成功即清零;除此之外只有手动重置能清。 |
cooldown_until |
冷却中或已降级的身份重新可被调度的时间。 |
minted_at、last_used_at、retired_at、retire_reason |
时间线。 |
note |
运维给它写的备注:只能一行,最多 200 个字符——是谁的账号、Cookie 从哪里来。请求链路上没有任何环节读它。见身份抽屉。 |
account_id、account_seen_at |
上一次检查登录态时平台给出的账号,以及检查的时间。还没有哪次检查给出过账号时为空。见检查登录态。 |
指纹里推断不出浏览器家族和主版本号的身份会被直接拒绝,而不是套一个默认值。一个与 User-Agent 自相矛盾的 TLS 指纹,比没有身份更糟糕,所以铸造和导入在这种情况下都选择拒绝而不是猜。
| 状态 | 含义 | 怎么进来 | 怎么出去 |
|---|---|---|---|
active(可用) |
当前可被调度。 | 刚铸造、刚导入、冷却到期,或被手动重置。 | 命中风控,或被退休。 |
cooling(冷却中) |
因命中风控而暂歇。 | risk_control 结果,按指数退避计算冷却时长。 |
冷却到期(在下一次调度时被提回),或冷却期间有一次请求成功。 |
degraded(降级) |
冷却已到上限。只有在没有任何可用身份时才会被启用。 | 算出的冷却时长达到或超过 sched.cooldown_max_seconds。 |
上限冷却到期后回到 active,但连续失败计数保留——所以在它成功之前,健康分会把它排在最后。 |
retired(已退休) |
已死。Cookie 已被清除,行本身保留用于统计。 | 你退休了它,或者它绑定的代理被删除了。 | 不会回来。已退休的身份不能重置、不能被指定、不会被调度。 |
minting(铸造中) |
数据库列的默认值。铸造路径不会把身份留在这个状态——一拿到 Cookie 就直接写 active——所以正常情况下你不会看到处于这个状态的行。 |
目前没有任何代码写入它。 | — |
每次请求结束后,结果都会回写到身份状态上:
| 结果 | 对身份的影响 |
|---|---|
ok |
清空连续失败计数。冷却中的身份被提回 active;已降级的只清计数,仍然要熬完它的观察期。 |
business_error(作品已删除或私密) |
什么都不做。 作品不存在是关于内容的事实,不是关于身份的事实。 |
network_error |
连续失败计数 +1。不冷却——请求根本没到达平台。 |
risk_control |
连续失败计数 +1,计算并写入冷却时间,状态置为 cooling 或 degraded。 |
冷却时长为 sched.cooldown_base_seconds × 2^(连续失败次数 - 1) × risk_weight,上限为 sched.cooldown_max_seconds,其中 risk_weight 来自接口策略(见熔断、令牌桶,以及熔断打开时该怎么办)。按默认值——基准 60 秒、上限 21600 秒(6 小时):
| 连续命中风控次数 | 权重 1.0 时的冷却 | 权重 1.8 时的冷却 |
|---|---|---|
| 1 | 1 分钟 | 1 分 48 秒 |
| 2 | 2 分钟 | 3 分 36 秒 |
| 3 | 4 分钟 | 7 分 12 秒 |
| 5 | 16 分钟 | 28 分 48 秒 |
| 8 | 2 小时 8 分 | 3 小时 50 分 |
| 9 | 4 小时 16 分 | 6 小时 —— 降级 |
| 10 | 6 小时 —— 降级 | 6 小时 —— 降级 |
健康度,以及两个含义完全不同的列
Section titled “健康度,以及两个含义完全不同的列”身份列表里有两个看起来都像是在给身份下判断的列,但它们量的是完全不同的东西。页面在工具栏下面用一句话说明了这一点——把这两个数当成一个来读,正是误诊身份池的典型方式。
**会话凭据(Credential)**是对已存 Cookie 的静态检查。不发请求、不看历史:会话 cookie 在不在?长度够不够,是真实会话还是首次访问拿到的引导值?
| 判定 | 含义 |
|---|---|
ok(完整) |
会话 cookie 存在,且长度足够。 |
missing(缺失) |
Cookie 里根本没有会话值。 |
too_short(仅引导值) |
这是 HTML 文档里写死的引导值,而不是平台自己的 JavaScript 替换上去的那个。带着它发出去的请求,即使签名完全正确也会被拒。 |
unknown(无法判断) |
Cookie 解密不了,或者该平台没有登记会话 cookie。 |
各平台用来判断的 cookie:
| 平台 | Cookie | 视为真实会话的最小长度 |
|---|---|---|
tiktok |
msToken |
144 |
douyin |
UIFID_TEMP |
32 |
bilibili |
buvid3 |
1——有就行 |
抖音的这个值不止一种写法:铸造出来的身份叫 UIFID_TEMP,从浏览器导出的登录态里叫 UIFID,另外几种大小写签名器也认。这一列按签名器读取的顺序找,取第一个有值的来判断,单元格里显示的是实际找到的那个名字;一个都没有时才显示 UIFID_TEMP。早先的版本只认 UIFID_TEMP,所以从浏览器导入的抖音登录态都被判成 missing(缺失),在列表里被当成会话已失效默认隐藏——而签名器其实一直在用它签名。
B 站的 buvid3 还没有测到过什么引导值,所以只要有就算数。早先的版本没给 B 站登记任何 cookie,每个 B 站身份在这一列都显示 unknown(无法判断)——在这一列上,它的意思是「本程序判断不了」,而对一个缺了就不让导入的 cookie 来说,这并不成立。
**健康度(Health)**衡量的是这个身份最近的请求实际跑得怎么样。这一列和调度器排序读的是同一张聚合表、同样的两个时间窗,所以你看到的数字和身份池采用的顺序出自同一个公式:
health = success_rate × (1 − risk_rate) × 0.5^consecutive_fails其中 success_rate 取最近 15 分钟,risk_rate 取最近 60 分钟,consecutive_fails 是连续失败次数——这三个也是你在 request_log 和 API 里会遇到的字段名。
三个因子是相乘而不是加权求和,因为它们量纲不同——在加权和里,无界的连续失败次数要么压倒一切,要么把一个自相矛盾的状态(「成功率 100%,但刚刚连续失败了三次」)平均掉。相乘意味着任何一个因子塌下去,整体分数就跟着塌。
- 15 分钟窗口内的样本少于 5 个时,成功率会用一个先验值代替,这样新身份既不会被当成久经考验的那样信任,也不会被压到队尾。页面上显示的这一列固定使用内置的
0.8;pool.health_prior调整的是调度器排序时用的先验值。把这个配置项改离默认值,改变的是冷启动身份被挑中的积极程度,而不是表格里给它显示的数字。 - 当聚合表里没有这个身份的记录时,这一列是空的(「无流量」)。这既不是通过,也不是 0,而是「一无所知」。此时页面退回显示连续失败次数。
- 时间窗读自
identity_health_5m连续聚合视图,这是一个 TimescaleDB 对象。在原生 PostgreSQL 上这次读取会静默失败,所有身份都显示为未测量;调度照常工作,只是仅按连续失败次数和最久未使用来排序。 - 排序时分数会被归入 5 个粗档位。按精确分数排序会让最健康的那一个永远胜出,那是热点而不是轮换。
身份页面(/identities)
Section titled “身份页面(/identities)”列表来自 GET /api/v1/admin/identities(按铸造时间倒序,limit 默认 100、最大 500,用 offset 翻页)。已退休的身份默认不返回,除非请求带了 include_retired、按状态 retired 筛选,或者用 identity_id 指名某一个。它们会保留 90 天;以前控制台把它们一并取回再自己藏起来,结果一个糟糕的星期退休掉的身份,就把更早导入的登录身份悄无声息地挤出了页面。meta.total 给出符合条件的身份总数,meta.retired 给出同样条件下被略去的已退休身份数。
控制台一次取 200 行,每 5 秒轮询一次。平台、状态、来源、登录态和轮换这几个筛选交给服务端;搜索框只在已经加载的行里查找,因为它要匹配的是人会输入的东西——代理的名称、当前语言下的平台名——而这些数据库里没有。它也匹配身份的备注和平台账号,在几个登录身份里找出某一个,靠的就是这个。表格下方会写「显示 N / M」,加载更多再取 200 行,最多到服务端上限 500;再往后就得缩小筛选范围。
筛选条件都放在地址里——/identities?platform=&login=&id=,另外还有 state、source、rotation、serves、q 和 all——所以总览页按平台的告警可以直接链接到某个平台的身份,请求日志可以直接打开某个身份的抽屉;刷新或后退也不会丢掉当前视图。login 取 yes 或 no。本版本不认识的值按「不筛选」处理,而不是给出一张空表,这样按更新版本写的链接也照样能打开。
serves=<接口> 在工具栏里没有对应的控件:它问的是「谁能服务这个接口」,是从接口那一侧问的,调度器页面的覆盖表就用它链接过来。它只保留轮换可以派到这个接口的身份——在所有接口上参与轮换的,以及清单里列了这个接口的,只服务点名请求的身份永远不在其中;接口从第一页起就需要登录时(比如抖音作者的喜欢列表)只算登录态——页面会在表格上方用一条提示说明这一点,旁边有清空按钮。没有这条提示,这样收窄过的表格看上去就像池子丢了大半身份。它只按轮换设置筛选,所以冷却中的身份也会列出来——除非同时带了 ready=1,从覆盖表的可分配点过来时就是这样:这时只列调度器此刻会派去的正常和降级身份,正好是那个数字数到的那些。
这个页面上的每个写操作都需要 operator 角色。低于这个角色时,铸造、导入、行内按钮、勾选行以及自动补充卡片的编辑项都会被隐藏,而不是置灰;无论哪种情况,真正把关的都是服务端。
自动补充卡片
Section titled “自动补充卡片”位于页面顶部,数据来自 GET /api/v1/admin/identities/pool:
- 每个平台一个数字,显示可用身份数——存活(
active+cooling)且连续失败次数低于pool.max_fail_streak的那些。刻意不用行数:一个每次请求都失败的身份依然算存活,所以按行数统计的池子可以一边停在目标值上、一边什么都服务不了。 - 每个平台一行,各有一个自动开关和两个数字:低于和补到。没有单独设置的平台沿用全局的
pool.min_size(默认 3)和pool.target_size(默认 8),仍在沿用的数字旁边会标着默认;把数字改回全局值,它就重新跟随全局;点恢复默认会一次清掉这个平台单独设置的两个数字。关掉自动会写入pool.<platform>.min_size = 0:这个平台不再自动铸造,也不再发身份池告警——只用一个平台的部署要的就是这个。保存时只写你改动的部分。开启自动时下限至少为 1,补到的数量不能低于下限。 - 每个平台一行登录态统计:轮换能用上的登录身份有几个,旁边列出保留的、冷却中的和限定接口的各有几个。如果某个平台有拒绝游客的接口(抖音作者作品的第一页之后、作者喜欢列表的每一页,B 站评论的第一页之后),而轮换里一个登录身份都没有,这一行会点名这些接口,说明它们会返回
IDENTITY_LOGIN_REQUIRED,并提供一个预设为该平台的导入登录态按钮。在有这一行之前,唯一导入的登录身份被保留了、进了冷却或者过期了,第一个信号就是这个错误。 - 每个平台一行铸造动态:当前正在铸造什么、最近 20 次尝试里属于这个平台的那些(一排小方块,鼠标悬停显示原因),以及连续失败把补充任务打入退避时的等待时间。之所以按平台分开,是因为 worker 对浏览器和普通 HTTP 两条铸造路径分别退避;以前共用一条记录,浏览器一挂,看上去就像 B 站也在失败。这些数据由 worker 经 Redis 提供,因为失败的铸造不会留下任何身份行——而这正是「死活补不上来的池子」以前看起来和「根本没人要求补充的池子」一模一样的原因。
- 如果没有设置
DTK_BROWSER_RPC_URL,抖音和 TikTok 就无法铸造:它们的数字输入框会消失,池子里只有你手动导入的身份,它们的阈值也不起作用。B 站那一行照常可编辑、照常补充,因为 B 站的访客身份不需要浏览器就能铸造(见 B 站身份)。卡片会直接说明这一点。
补充任务本身跑在 worker 里:每分钟检查一次各平台,每次只铸造一个身份,并持有跨进程锁,这样 --scale worker=N 不会把一次铸造变成 N 次。因此从下限补到目标值需要几分钟。这个节奏是设计的一部分——一秒之内从同一个部署冒出五个新访客,比这些身份之后发出的请求更刺眼。
这个任务还会拒绝往一场平台级事故里补充:当所有存活身份都在失败时(可用数为 0 而存活数不为 0),它会按住不动,让身份池告警去说话,而不是再送几个新身份进去一起被烧。
| 列 | 说明 |
|---|---|
| 状态 | active / cooling / degraded / retired;轮换没有在所有接口上分配这个身份时,徽标下方会标出保留或限定接口。 |
| 健康度 | 实测分数,或「无流量」。 |
| 平台 | 抖音、TikTok 或 B 站,显示平台名。 |
| 会话凭据 | 上文说的静态会话检查。 |
| 身份 ID | uuid,列宽不够时会截短显示;点一下复制的是完整 ID,抽屉里也会完整打印。不可隐藏——ID 是你要粘进 ?identity= 的那个值。 |
| 备注 | 运维给这个身份写的备注,只占一行,过长时截短,悬停看全文;没有时显示 —。单独成列,而不是像以前那样挂在 ID 下面——在那里它读起来像 ID 的一部分,没人注意到;区分三个登录身份,一眼看的就是它。可以排序,搜索框也会匹配它。 |
| 代理 | 代理有名称时显示名称,没有时显示代理 id,没有代理时显示「服务器出口」。从不显示 URL,哪怕是掩码后的。 |
| 类型 | 先是铸造或导入,再是游客或已登录(带锁图标)。登录身份还会标出何时过期——「3 天后过期」「5 小时后过期」或「已过期」,悬停看具体日期——一周之内会高亮,因为这正是该重新导入的时候。日期读自会话 cookie(sid_guard,或 B 站的 SESSDATA),所以被平台提前终止的会话仍然显示原来的日期;要确认这一点,用检查登录态。按这一列排序时登录身份排在前面,最早过期的最先。以前来源和登录态各占一列,两列加起来只为显示两个词。 |
| 冷却剩余 | 剩余时间,或 —。 |
| 最近使用 | 相对时间,悬停看精确时间。 |
| 连续失败、铸造时间 | 默认隐藏,可从列菜单打开。 |
轮询时,只有状态、会话凭据判定、连续失败次数或健康度档位发生变化的行才会闪一下——分数的细微漂移不算,因为每次轮询每一行都闪的表格只会教会你无视这个提示。
默认视图隐藏了什么
Section titled “默认视图隐藏了什么”默认情况下表格会隐去两类行,并在表格上方给出计数和一个显示全部按钮(地址里是 all=1):
- 已退休的身份——由服务端略去,计数取自
meta.retired;显示全部会带上include_retired把它们取回来。凭证已被清除,无法承接请求。 - 会话凭据判定为
missing或too_short的身份——由浏览器在已加载的行里藏起;即使签名正确,平台也会拒绝它们。
冷却中和已降级的身份不会被隐藏。把一个正在恢复的池子藏起来,正是控制台向一位手握四十个身份的运维报告「没有身份」的方式。显式按状态筛选 retired 时,这些行照常显示。
- 测试——以该身份排队发出一次真实的签名请求,然后展示结果。见测试、重置与退休。
- 重置——只在真的有事可做时才出现:身份处于冷却或降级,或者连续失败次数不为 0。
- 退休——对已退休的行是禁用状态。如果退休的是某个平台在轮换里的最后一个登录身份,确认框会点名之后将返回
IDENTITY_LOGIN_REQUIRED的接口。 - 勾选若干行后,工具栏会出现重置、设置 N 个身份的轮换、导出 N 个和退休。它们只作用于当前筛选下仍然可见的已选行——以前勾选会跨筛选保留,结果作用到了谁也看不见的行上——并以「成功 n 个,失败 m 个」报告结果,而不是在第一次被拒时就停下。
- 设置 N 个身份的轮换给选中的每个未退休身份应用同一个轮换设置,选项和抽屉里的三个一样(见让身份退出轮换)。不然为作者作品买的三个登录账号,就得进三次抽屉、点三遍同样的按钮。只有选中的身份都属于同一个平台时才能选接口清单,因为接口名只属于一个平台;跨平台时只能在「所有接口」和「只服务点名它的请求」之间选。如果选中的身份包含了某个平台在需要登录的接口上参与轮换的全部登录身份,而这次设置会把它们从这些接口上撤下,对话框会点名之后在需要登录的地方将返回
IDENTITY_LOGIN_REQUIRED的接口,并把按钮变成红色。它只提醒,不拦着。 - 点击行会打开抽屉,并把这个身份的 id 写进地址(
?id=)。
点击任意一行会打开抽屉,它回答两个问题:这个身份是什么,以及它一直在干什么。
抽屉从轮询中的列表里读取这一行,所以重置、退休或冷却结束都会直接反映在抽屉里,而不只是在它背后的表格上;如果链接指名的身份不在已加载的那一页里,抽屉会单独去取它。顶部依次是平台、连续失败次数、最近使用、冷却(如有)、登录态过期时间,已退休的身份还会写明何时、因何退休。下面是操作:复制 ID、在日志中查看(按这个身份筛选的请求日志,/logs?tab=requests&identity_id=),operator 还能看到测试、需要时才出现的重置、导出和退休——和行内操作一样,这样读完一个身份为什么失败之后,不必关掉抽屉去找下一步的按钮。探测的结果、HTTP 状态和耗时显示在它们下方。
顶部那排信息下面是这个身份的备注和平台账号。以前导入三个登录身份就是三个 UUID,要知道哪个是哪个账号,只能记住导入的先后顺序。
- 备注归运维所有:只能一行,最多 200 个字符——是谁的账号、Cookie 从哪里来。点添加备注(已有备注时点编辑)就地打开编辑框;回车保存,Esc 取消且不会关掉抽屉,存成空白就是清除。低于 operator 角色时只显示、不能编辑。表格里它有单独的一列,紧挨着身份 ID,搜索框也能搜到它;它还会出现在请求日志里的身份下方,以及调试台身份选择框每个选项的最前面。请求链路上没有任何环节读它。已退休的身份保留备注,而且仍然可以修改——「这原来是哪个账号」恰恰是事后有人会问的问题。
- 平台账号是上一次检查登录态时平台说这些 Cookie 属于的账号,以及检查的时间——见检查登录态。两者从两侧回答同一个问题:备注是有人认为的,账号 ID 是平台答复的,所以备注过时了、或者同一个登录身份被导入了两次,照样看得出来。
备注用 PATCH /api/v1/admin/identities/{id} 写入,请求体 {"note": "..."},{"note": null} 表示清除。这个字段是必填的,这样空的请求体就不会被理解成清除;含换行、或者去掉首尾空白后超过 200 个字符,会以 INVALID_PARAM 被拒。它需要 operator 角色,返回这个身份在列表里的样子;每次修改都以 identity.note_changed 记入审计——只记修改前后有没有备注,从不记内容,因为备注是自由文本,总会有人把登录账号绑定的手机号写进去。
Cookie 清单
Section titled “Cookie 清单”「这个身份由什么组成」卡片显示平台、来源、是否带登录态、铸造时间,以及指纹里的浏览器和时区。下面是来自 GET /api/v1/admin/identities/{id}/cookies 的逐条 Cookie:名称、角色、一句说明这条 cookie 是干什么的、字符长度,以及掩码后的值(首四位和末四位,中间是固定长度的星号——固定,是因为可变长度会泄露原值长度)。
长度出现在这里、而在列表页刻意不显示:一个被截断的 token 和一个错误的 token 从外面看完全一样,长度正是区分它们的东西。
| 角色 | 含义 |
|---|---|
required(必需) |
本程序拒绝使用缺少它的身份。目前抖音和 TikTok 是 ttwid,B 站是 buvid3。 |
session(登录态) |
携带登录会话:sessionid、sessionid_ss、sid_tt、sid_guard、uid_tt、sid_ucp_v1,以及 B 站的 SESSDATA。B 站的 bili_jct 和 DedeUserID 也归在这里,作为登录态的一部分显示,尽管它们单独都证明不了已登录。 |
useful(有用) |
不是必需的,但带上它的身份被拒的次数会少一些:odin_tt、s_v_web_id、msToken、passport_csrf_token、tt_csrf_token、__ac_nonce,B 站的 buvid4、b_nut、buvid_fp 和 _uuid,以及本程序签名器会读取的那些 cookie。 |
other(其他) |
平台下发的,原样保留。本程序不读它。 |
每条 cookie 的说明按 cookie 名索引,找不到时回落到该角色自己的那句话,所以一个本程序从没见过的 cookie 名也能得到一个诚实的回答,而不是一片空白。
有两种横幅会取代这个列表:
- 这份 Cookie 用的密钥本实例已经没有了——密钥轮换时没有重新加密。这个身份同样无法再签名,退休它。
- 已退休的身份会清空 Cookie——没有内容可看。
显示真实内容,以及它会被审计这件事
Section titled “显示真实内容,以及它会被审计这件事”显示真实内容会调用 GET /api/v1/admin/identities/{id}/cookies/reveal,把即将发出的 Cookie 原样打印出来,并提供两个复制按钮:复制成 Cookie: 请求头(给 curl 用)或复制成 JSON。
这是「任何响应都不携带 Cookie」这条规则的一个刻意的例外,而且它是一条独立的路由而不是一个开关参数,理由如下:
- 它要求 operator 角色以及
admin或identity:manage权限——比那份掩码后的清单更严的门槛,后者任何管理侧只读用户都能加载。 - 它会写一条审计记录(
identity.cookies_revealed,记录谁读的、返回了多少条 cookie)。正因为它是独立路由,这条记录不会被抽屉每次打开都要加载的掩码视图淹没。 - 它取代的替代方案是开个 shell 手动解密:同样的泄露,却没有审计记录,也没有权限检查挡在前面。
把显示出来的 Cookie 当成凭证对待。带登录态的那份等于别人的账号。
身份上的 authenticated 说明的是粘贴内容里有会话 cookie。平台是否还认它,是另一回事,而这两者不一致时唯一的症状是:本该有的登录态数据在原本成功的响应里悄悄消失。检查登录态会去问平台(POST /api/v1/admin/identities/{id}/session,以任务方式排队),并给出:
| 原因 | 含义 |
|---|---|
live |
平台确认登录态有效,并给出了账号。 |
signed_out |
没有有效登录态。TikTok 对「从来没登录过」和「登录已失效」的回答完全一样,所以分不出是哪一种。 |
indeterminate |
仅抖音。它的接口对访客和登录态的回答完全相同,都会给出一个自己的 uid。这次能确认的只是:Cookie 到达了平台并被认了下来——不是它已登录。 |
refused |
平台拒绝了这次检查而不是回答它。这一次说明不了会话的状态。 |
unreachable |
完全没有拿到回应。是网络或代理的问题,不是会话的问题。 |
平台给出了账号时,这次检查会把它存到身份上,记为 account_id,时间记在 account_seen_at,抽屉里显示为平台账号。没给出账号的检查——refused、unreachable——不会动已存的那个:它说明不了这些 Cookie 是谁的,也不该抹掉之前的检查已经知道的东西。抖音的 ID 是和 indeterminate 一起给出的,游客和登录身份都有,所以在抖音上它确认的是平台认出了这个客户端,而不是已登录。这个 ID 的用处是区分身份:两个备注相同的登录身份,或者同一个账号导入了两次,显示的是同一个 ID。
这个检查走的是正常的请求链路,所以它像真实流量一样消耗这个身份,但不会把结果记到身份的健康状况上。账号 ID 是关于这些 Cookie 的事实,不是对它们的评判。
抽屉会读取该身份最近一周的 request_log 记录(最多 200 条)并展示:
- 一条横幅,直接点明故障的形状。只有某一个接口拒绝、其他接口照常响应,通常是本程序在那条路径上的签名有问题,或者那是个需要 visitor id 的签名保护接口——不是 Cookie 废了。所有接口都拒绝,那就是身份本身的问题。
- 按接口的统计:调用次数、其中多少次被拒、以及错误码。
- 最近 25 次请求,含结果、HTTP 状态和耗时。
铸造抖音和 TikTok 身份会驱动一个真实的无头浏览器、经由该身份自己的代理完成,因为 Cookie 和指纹必须来自后续真正使用它们的同一个会话。出口的时区和语言会和代理的 GeoIP 对齐:一个德国的代理配上 Asia/Shanghai 的时钟,是白送出去的破绽。屏幕尺寸不由地理位置推导——真实浏览器报告什么就是什么。B 站不用浏览器,也守着同一条规则,见 B 站身份。
它需要 browser-rpc 容器,这个容器按设计是可选的,也是整个栈里最重的一个服务:
echo 'DTK_BROWSER_RPC_URL=http://browser-rpc:9000' >> .envdocker compose -p dtk -f docker/compose.yml --profile browser up -d --build这里有两件事必须同时成立,而且失败方式各不相同。容器要跑起来(profile)。还要告诉 api 和 worker 它在哪里(.env 里的 DTK_BROWSER_RPC_URL)。镜像装哪个 CloakBrowser 版本由 docker/compose.yml 决定,所以用 Compose 构建时不用再设别的,镜像里就有浏览器。为什么钉的是 commit 而不是 tag、以及怎么换一个版本,见安装的「构建浏览器镜像:CloakBrowser 版本固定」一节。
这些都没有的话,抖音和 TikTok 的池子里就只有你手动导入的身份——这是受支持的运行模式,不是错误。B 站照常铸造。
在控制台里,铸造身份需要选择平台、数量(1 到 10),以及可选的铸造出口。每个身份作为一个独立任务提交(POST /api/v1/admin/identities/mint,返回 202),对话框会逐个等待,每个耗时以秒计——几十秒是正常的。铸造在整个部署范围内串行执行,所以一次要五个身份就是五次接连的铸造;单个任务最多等 180 秒来抢锁,超时就放弃。对话框只列出本实例能铸造的平台(按每个平台的 can_mint),其余平台会点名并建议改为导入;结束时给出一条汇总——「已铸造 3 个,失败 2 个」以及 worker 给出的原因——而不是每失败一次弹一条提示。
铸造没有产出时,任务会带着一个原因码失败:
| 原因 | 含义 |
|---|---|
no_free_proxy |
每个代理后面都已经有一个该平台的存活身份了。同一出口后面放两个正是身份池要避免的重组,所以任务选择等待而不是叠加。加代理。 |
proxy_not_found |
本次铸造指定的代理已经不存在了。 |
proxy_undecryptable |
代理 URL 解密失败(DTK_SECRET_KEY 不对)。改用直连铸造会把这个身份绑死在一个它永远不会再用的出口上。 |
rpc_unavailable |
浏览器容器没有响应。 |
unusable_fingerprint |
浏览器返回的指纹里没有可用的版本号,与其配一个猜出来的 TLS 档案,不如直接拒绝这个身份。 |
mint_refused |
仅 B 站:站点这一次没有发放访客会话。补充任务会退避后重试。 |
busy / locked |
另一次铸造持有锁。可重试;API 返回 429,Retry-After 为 30 秒。 |
自动铸造失败会让补充任务按指数退避慢下来(60 秒起,翻倍至 1 小时上限,下一次成功即清除)。手动按钮触发的铸造失败刻意不碰这个计数器,这样对着一个挂掉的浏览器连按几次,也不会把自动补充停掉一小时。退避按铸造路径分别计算——浏览器一条,B 站的普通请求一条——所以浏览器容器挂了,不会再连带停掉 B 站的补充。
导入你已经有的 Cookie
Section titled “导入你已经有的 Cookie”导入 Cookie 接收你从浏览器里复制出来的一份 Cookie。在没有浏览器容器的部署里,它就是抖音和 TikTok 的整个身份池,也是让这个实例拥有登录态会话的唯一途径。
四种可接受的粘贴格式
Section titled “四种可接受的粘贴格式”格式是自动识别的,不需要你声明——要求用户先说清自己手上是四种格式中的哪一种,正是导入流程劝退人的方式。
| 格式 | 长什么样 | 从哪来 |
|---|---|---|
header |
ttwid=a; odin_tt=b; sessionid=c |
DevTools → 复制请求头。开头的 Cookie: 会被去掉。 |
json_array |
[{"name": "ttwid", "value": "..."}, ...],或者一个普通的 {"名称": "值"} 对象 |
Cookie 管理类浏览器扩展。 |
netscape |
制表符分隔、7 个字段、# 注释 |
cookies.txt 导出文件。 |
loose |
每行一个 name=value |
手工复制。同时也是兜底:当格式识别猜错、选中的解析器什么都没解出来时会回落到它。 |
整段粘贴内容上限为 200000 字符。
预览会告诉你什么
Section titled “预览会告诉你什么”在输入框里打字会触发一次防抖的服务端试运行(dry run),所以你看到的是服务端对这段粘贴的真实理解,而不是浏览器的猜测。在存下任何东西之前,它会报告:
- 识别出的格式;
- 这些 cookie 名指向哪个平台。和对话框里选的平台不一致时,它会说出来,并提供**切换到……**按钮——只是提供,不会替你切,因为在别人还在输入时切换平台,会把他的粘贴内容重新判一遍。以前对话框不管粘贴了什么都停在抖音上,于是一份 B 站 Cookie 会按抖音的必需 cookie 来判,因为缺一个它本来就不会有的
ttwid而被拒。每个平台的输入框下方也会列出它需要哪些 cookie; - 每个 cookie 的名称、服务端按所选平台给出的角色,以及掩码后的值(控制台从来拿不到真实值);
- 从你填的 User-Agent 推断出的浏览器家族与主版本号。Edge 和 Opera 会映射到 Chrome,那是它们真正的 TLS 档案;
- 这份 Cookie 是否带登录态;
- Cookie 带登录态时,从
sid_guard(抖音、TikTok)或SESSDATA(B 站)里读出的过期时间; - 缺失的必需 cookie——没有
ttwid(抖音、TikTok)或buvid3(B 站)根本存不进去; - 警告,以稳定的代码加一句话的形式给出:
no_cookies、logged_in_session、unknown_browser、no_useful_cookies。
另外三个字段值得填:
- User agent——来自同一个浏览器会话。抖音和 TikTok 都会把它哈希进签名,所以换一个 UA 发出去的 Cookie 是它们看得见的自相矛盾。不填就推断不出浏览器,导入会被拒绝。
- 代理——这个账号平时登录所用的出口。导入的 Cookie 和铸造出来的一样,与它绑定终身。
- 备注——可选:这是谁的账号,或者 Cookie 从哪里来。只能一行,最多 200 个字符;凡是显示这个身份的地方都会在 ID 旁边显示它,之后也可以在抽屉里修改。导入接口里对应的字段是
note。
POST /api/v1/admin/identities/import 还接受 language 和 timezone;控制台没有为它们提供输入框。
存入之后,对话框会关闭并打开新身份的抽屉;如果是登录身份,还会立刻执行一次检查登录态——平台认不认这个会话,是导入之后唯一悬而未决的问题。成功提示里带一个复制 ID 按钮,方便填进 identity=。
带登录态的 Cookie 不等于访客身份
Section titled “带登录态的 Cookie 不等于访客身份”对话框顶部始终有警告;粘贴的是登录态 Cookie(或者恢复文件里含有登录身份)时,底部还会要求勾选一个确认框才肯存入。两者都不是装饰。确认框只对登录态出现:让人对着一个游客的访客 ID 发誓「这等于密码」,只会教会他不看内容就打勾。
- 一份带登录态的 Cookie 等同于账号密码。 拿到它的人就能以那个账号的身份行事。它加密存储,没有任何接口会返回它,但本实例的管理员可以使用它。
- 用一个专门为此准备的账号,绝不要用你的主账号。
- 账号有被封禁的可能。 各平台一般不会因为读取数据而封号,但不排除封号的可能。导入登录态、使用依赖登录态的功能,后果和风险由你自行承担;对话框底部的确认框,就是让你确认这一点。
- 身份池是全实例共享的,行本身没有归属人。因此在请求里指定身份(
?identity=<uuid>)需要 operator 角色以及admin或identity:manage权限——一把普通的只读 Key 不该能碰到别人的登录会话。见 REST API 指南。 - 指定了身份的请求绝不会改由其他身份来发,也绝不会走响应缓存。只有来自那个会话的答案才是正确的。
- 导入的登录态会话到期前 3 天,worker 会发出告警(
cookie_expiring通知),依据是从sid_guard(或 B 站的SESSDATA)里读出的过期时间。这把一次静默的集体失效,变成了提前几天的预警。
访客身份便宜且用完即弃:烧掉一个,池子再铸一个。带登录态的身份不是——这也是为什么围绕它的每一处设计(显示时的审计记录、指定身份的权限门槛、到期预警)都要付出代价。
让身份退出轮换
Section titled “让身份退出轮换”在请求里指定身份,只会让那一次请求用它,管不到其余的请求:调度器照样会把这个账号分给其他所有请求,而正是这些请求最容易让登录账号被风控。身份抽屉里的 轮换 卡片决定轮换可以把这个身份用在哪里,在轮换可以把它用在哪里下面三选一:
| 选项 | 存下的 endpoints |
轮换怎么用这个身份 |
|---|---|---|
| 所有接口 | null |
可以分配给任何接口——这是默认值。需要登录的接口照旧优先或只用登录身份;开启游客优先时,其余接口只有在没有游客身份可用时才会用到登录身份,见哪些内容需要登录身份。 |
| 只用于下面勾选的接口 | 接口名列表,必须属于该身份所在的平台 | 只在勾选的接口上分配它,其他接口都不会用到。要把登录身份只留给需要登录的接口,就这样设置。表格里它会标成限定接口。 |
| 只服务点名它的请求 | [] |
轮换永远不会分配它,需要登录的翻页也不会。卡片会显示用来点名它的 identity=<id>,表格里它会标成保留。 |
在这张卡片提供中间那一项之前,控制台只有一个开关,对应的是最后一项。想把登录身份留给作者作品的人打开了它,结果作者作品反而不再用它了:作者作品的请求从来不会点名身份,而空列表的意思就只是「只服务点名它的请求」。
选中间那一项会展开该平台的接口清单。游客在其中会吃亏的接口带有标记——需要登录、翻页需要登录或登录后更完整,鼠标悬停会说明游客拿到的是什么;目前带标记的是抖音的作者作品,以及 B 站的评论和评论回复,三个都是翻页需要登录;还有抖音的作者喜欢列表,需要登录。只选需要登录的接口只勾上带标记的那些;全选和清空就是字面意思。只回答发起请求的身份本人数据的接口——收藏,以及抖音的收藏夹——不在清单里:它们总是需要 identity=,无论清单怎么写,轮换都不会用到它们。如果通过 API 设置的清单里有本版本不提供的接口,它会以已勾选的状态显示,这样保存时不会把它丢掉。
点保存之前什么都不会改,取消会恢复已存的设置;两个按钮在有改动之后才出现。要把某个平台在轮换里的最后一个登录身份从只能用登录身份的接口上撤下,会先问一句——就在卡片里问,而不是在抽屉上面再弹一个对话框,因为两者都响应 Esc,以前取消这个问题会连抽屉一起关掉。它会点名之后在需要登录的地方将返回 IDENTITY_LOGIN_REQUIRED 的接口(除非请求点名了身份),点仍然修改才会执行。要一次设置多个身份,在表格里勾选它们,再用设置 N 个身份的轮换(见行内操作)。
对一个退出了轮换的身份——保留的是整个退出,限定接口的是在没勾选的那些接口上退出:
- 轮换在那里不再分配它。 未指定身份的请求、自检和临时的
dtk fetch都会跳过它。 - 带
identity=的请求照常使用它,任何接口都行,只要它没有退休。保留它就是为了这个。 - 它不计入身份池水位,限定接口的和保留的都一样。补充任务只统计轮换能在所有接口上使用的身份,所以会为一般流量另外铸造一个,而不是把一个只留给某个接口、或者哪个接口都不给用的账号算作容量。
- 它不随导出一起带走。 这是对这个身份池的决定,不属于身份本身。
- 它不是后备。 某个接口能用的身份只剩被排除在外的那些时,未指定身份的请求会返回
IDENTITY_POOL_EXHAUSTED,而不是借用其中一个;details.reserved写明跳过了几个。所以身份池页面一排可用、请求却报这个错,是这个设置在起作用,不是调度器出了问题。
导入对话框把最后一项做成了一个复选框 只服务点名它的请求,导入接口则接受 reserved: true:身份一存进去就不参与轮换,这样为「只服务点名它的请求」而导入的登录身份,从来不会被分给别的请求——哪怕是在存入和保留之间的那一瞬间。导入的是登录身份、而这个平台没有别的登录身份在轮换里时,勾上它会点名在需要登录的地方仍将返回 IDENTITY_LOGIN_REQUIRED 的接口。
卡片和批量对话框写的都是 POST /api/v1/admin/identities/{id}/rotation,请求体 {"endpoints": ...},取值见上表;列表里的接口必须属于该身份所在的平台。它需要和重置、退休相同的角色,每次修改都会记入审计日志。
哪些内容需要登录身份
Section titled “哪些内容需要登录身份”抖音给访客身份看的作者作品,比给已登录账号的少。2026-10-17 实测:
| 请求 | 访客拿到什么 | dtk 怎么做 |
|---|---|---|
/api/v1/douyin/user/posts 的第一页 |
有结果,但缺作者最新的几条作品 | 轮换里有登录身份就一律用登录身份。一个都没有时由访客应答,结果带 meta.guest_fallback: true,并且这份结果不进缓存 |
之后的任何一页(cursor 不为 0) |
{"status_code":0},别的什么都没有 |
只用登录身份。一个都没有时立即返回 IDENTITY_LOGIN_REQUIRED(503)——不会再派访客去挨拒 |
作者喜欢列表更进一步:游客什么都拿不到。2026-10-20 实测,十五个作者里有一个公开了喜欢列表:
| 请求 | 访客拿到什么 | dtk 怎么做 |
|---|---|---|
/api/v1/douyin/user/likes 的任何一页 |
零字节——和私密列表的回答一样,公开的也是 | 只用登录身份。一个都没有时立即返回 IDENTITY_LOGIN_REQUIRED(503)。私密列表返回 NOT_FOUND,登录身份问也一样,和以前游客问时相同 |
5.1.6 之前这类请求交给游客,所以每个作者的喜欢列表看起来都是私密的。
B 站的视频评论也是这样。2026-10-18 实测:
| 请求 | 访客拿到什么 | dtk 怎么做 |
|---|---|---|
/api/v1/bilibili/video/comments 的第一页 |
最热的三条评论,并被告知这已经是最后一页 | 轮换里有登录身份就一律用登录身份。一个都没有时由访客应答,结果带 meta.guest_fallback: true,并且这份结果不进缓存 |
| 之后的任何一页 | 什么都没有——它已被告知第一页就是最后一页 | 只用登录身份。一个都没有时立即返回 IDENTITY_LOGIN_REQUIRED(503) |
TikTok 的搜索也是这样。2026-10-20 实测:
| 请求 | 访客拿到什么 | dtk 怎么做 |
|---|---|---|
/api/v1/tiktok/search/posts 的第一页 |
很薄的一页——一个关键词只有一条,另一个八条——不管实际有多少作品匹配 | 轮换里有登录身份就一律用登录身份。一个都没有时由访客应答,结果带 meta.guest_fallback: true,并且这份结果不进缓存 |
/api/v1/tiktok/search/users 的第一页 |
十个账号,并被告知还有更多 | 和下面那些接口一样,游客优先 |
| 两者之后的任何一页 | 什么列表都没有 | 只用登录身份。一个都没有时立即返回 IDENTITY_LOGIN_REQUIRED(503) |
登录身份的 TikTok 搜索还没有实测过,登录能多拿到多少目前不清楚。
其余接口给游客的结果和给登录身份的一样。在抖音,这指单条作品、主页、评论及其翻页;TikTok 上除搜索以外的全部;在 B 站,则是除评论列表、以及评论回复第一页之后以外的一切——游客能拿到回复的第一页(最多 20 条),之后就什么都没有了,所以回复的翻页和评论一样需要登录。音乐、话题、合集、直播间(按链接查、按场次查、一次查多个,以及抖音直播间的观众榜)、相关推荐和热榜,以及 B 站的搜索,只用游客实测过:游客拿到的是完整结果,所以也按同样的方式处理。游客唯一拿不到的,是观众榜上每个名次旁边的分值,所以分值对谁都不返回。
这些接口由游客优先(pool.<platform>.guests_first)来分配,默认开启,按平台分别设置。游客身份铸造一个不花什么,一分钟就能补上;登录身份是某个人的账号,把它花在游客也能答得一模一样的请求上,只会让它在真正需要登录的接口上更早用坏。所以轮换先试游客——可用的游客,没有可用的就用降级的,因为游客失败了代价是再铸一个,登录身份失败了代价是那个账号——游客之后才轮到登录身份。
登录身份是请求接着去试的第二层,而不是等游客全部消失才会动用的备用池。游客正在处理请求或配额用完时,会把请求让给空闲的登录身份,所以这个设置不会让一个本可以由空闲登录身份服务的请求被拒绝;它只决定谁先上,只有登录身份的池子也能照常工作。关掉之后,登录身份和游客按最近使用时间平摊这些接口,和这个设置出现之前一样。需要登录的接口根本不读这个设置:无论开没开,它们都照旧优先或只用登录身份,而且接口优先使用的登录身份忙时会等它,不会换成游客。
这个设置就是调度器页面覆盖卡片上的那个开关(见调度器页面)。worker 每个请求都会读它,所以改了不用重启。
在接口还没有标明游客会少拿到什么的时候,轮换不看身份有没有登录。抖音的第 2 页被派给访客,回来 17 个字节的空壳,被报成 UPSTREAM_RISK_CONTROL 并让那个访客进入冷却,而导入的登录身份一直闲着。第一页同样来自访客,所以缺最新的作品。
一个身份算不算已登录,取决于导入时它的 Cookie 里有没有会话 Cookie(sessionid 之类,B 站是 SESSDATA)——也就是类型列里的已登录,以及表格的登录态筛选。之后登录过期了它仍然算,被拒时照样进入冷却;抽屉里的检查登录态会告诉你是哪种情况。
想读完整的作者作品列表:
- 导入一份登录态 Cookie,用专门为此准备的账号。
- 让它只服务需要它的接口。在它抽屉里的 轮换 卡片上选只用于下面勾选的接口,点只选需要登录的接口,再点保存。要一次设置几个登录身份,就在表格里勾选它们,用设置 N 个身份的轮换。之后轮换只把作者作品交给它们,评论和单条查询仍然走游客,表格里它们会标成限定接口。开启游客优先时,在所有接口上参与轮换的登录身份本来就只在没有游客可用时才会被派到别的接口;限定接口还能在游客都没了的那天——浏览器容器挂了、池子烧光了——让它远离那些接口,并把它移出身份池水位,让补充任务去铸造游客承担其余的流量。用 API 也可以:
POST /api/v1/admin/identities/{id}/rotation,请求体{"endpoints": ["douyin.author_posts"]}。 - 做好它成为瓶颈的准备。所有作者作品请求都由这一个账号逐个处理,受它的令牌桶限制;而飞快地翻很多作者的作品,正是会让账号被风控的那种模式。要加的是账号,不是速度。
IDENTITY_LOGIN_REQUIRED 带着 details.cooling,即这个请求本可以用、但正在冷却的登录身份数。0 表示这个接口在轮换里没有登录身份:没导入过,或者它被设为不参与轮换。大于 0 表示等一等,或者检查它们的登录是否已过期。调度器页面的覆盖卡片会按接口显示轮换能派过去的登录身份有几个、冷却中的身份有几个——在请求失败之前就能看到。
有些列表属于发出请求的那个账号,而不属于请求里指名的任何人:抖音和 TikTok 的收藏(/user/bookmarks),以及抖音的收藏夹(/user/collections)。这类请求从不走轮换。用 identity 指明你要读的是哪个导入身份的列表,否则请求会以 identity 字段上的 INVALID_PARAM 被拒;结果也不进缓存,因为它只属于那个账号。没有这条规则时,不带 identity 的调用会返回轮换恰好抽到的那个账号的列表,共享缓存还会把它交给下一个调用方。B 站两者都没有:它不提供 /user/bookmarks,而它的 /user/collections 指名一位作者、列出这位作者公开的收藏夹。
B 站的访客会话靠普通 HTTP 就能拿到,而不是由页面脚本交给浏览器,身份池也按这一点区别对待它。
- 不需要浏览器就能铸造。 访客的
buvid3和buvid4就是对一个请求——站点自己的/x/frontend/finger/spi——的回答。这个请求经由本实例自己的、模拟浏览器 TLS 指纹的传输层发出,并走该身份的代理。创建会话的仍然是之后消耗它的同一个客户端、同一个出口——这正是浏览器路径守的那条规则。所以一个没有浏览器容器的安装照样能维持 B 站身份池:补充任务、铸造身份对话框和dtk identity mint都能铸造它,自动补充卡片把 B 站显示为可铸造;而且补充任务的退避按铸造路径分开计算,浏览器容器挂了也不会拖住 B 站的补充。 - 它自己的水位线。
pool.bilibili.min_size和pool.bilibili.target_size的规则与其他平台相同:-1沿用全局值,0关闭自动铸造。 - 导入。 B 站的 Cookie 必须有
buvid3,否则存不进去。SESSDATA表示这是一份登录态会话——像对待账号密码一样对待它——它还自带过期时间,表格会显示,cookie_expiring告警也据此发出。buvid4、b_nut、buvid_fp和_uuid值得一起带上。 - 检查登录态。 检查登录态会去问 B 站的
/x/web-interface/nav,它能真正区分出有效登录:回答isLogin: true时报告live,并给出账号的 mid;访客或已失效的会话报告signed_out。 - 登录身份用在哪。 评论:访客只拿到最热的三条,别的什么都没有(见哪些内容需要登录身份)。评论回复的翻页同样需要登录。想让导入的账号只服务这些,轮换卡片上的只选需要登录的接口会勾上
bilibili.comments和bilibili.comment_replies;上文的轮换接口接受同样的列表。 - UP 主投稿列表是 B 站防得最严的接口。 从同一个出口对
/api/v1/bilibili/user/posts连续发一串请求,接下来几分钟拿到的都是 HTTP 412 页面,无论请求头怎么写(实测);本实例把它报成UPSTREAM_RISK_CONTROL,并让该身份进入冷却。它的令牌桶是所有 B 站接口里最慢的;有用的是把请求错开,以及让身份走代理。见故障排查。
导出与恢复身份包
Section titled “导出与恢复身份包”勾选若干行后按导出 N 个,会调用 POST /api/v1/admin/identities/export,并把 dtk-identities-<时间戳>.json 存到你的下载目录;抽屉里的导出对单个身份做同样的事。超过 200 个时会拆成若干个 200 个一份的文件(-part-1.json、-part-2.json……),因为一次导出调用和一次恢复都最多接受这么多。用它把一个能用的池子搬到第二个实例、在做有风险的改动前留个副本,或者把某一个身份交给别人排查问题。
落到磁盘上的是一个凭证文件。 里面每一份 Cookie 在平台使其失效之前都是能用的,带登录态的那份等于别人的账号。文件里带一个 warning 字段,好让日后捡到它的人知道自己拿的是什么;导出操作按身份逐个写入审计。
- 每次调用 1 到 200 个身份;响应里包含每一份完整的 Cookie,且在内存中组装。
- 已退休的身份会以一个空 Cookie 的条目导出,而不是被丢掉——悄悄返回得比你要的少,只会让你在恢复到一半时才发现。
- 每个条目都带着身份的
note。它是标签而不是凭证,也是文件里唯一能说明某个登录身份是谁的账号的东西。轮换设置不会带走:恢复出来的身份在所有接口上参与轮换,直到你另作设置。
恢复走的是同一个导入对话框:在从文件恢复处选择文件,粘贴表单会随即关闭——恢复一个池子和手打一份 Cookie 是两件不同的事,一个靠猜来判断你想干哪件的对话框,一定会在最要紧的那天猜错。载入的文件会先做一次试运行,对话框在写入任何东西之前就列出哪些能恢复、哪些会被跳过,以及原因。
身份包路由(POST /api/v1/admin/identities/import/bundle)会带上每个条目原本的指纹。这正是它存在的意义:把导出的 Cookie 重新走一遍普通导入,浏览器信息只能从一个已经不在旁边的 User-Agent 推断,恢复出来的身份就会用一个不同的指纹去签名——而这恰恰是风控在找的差异。
version是被检查而不是被信任的。由更新版本写出的文件会被拒绝,而不是靠猜去读。当前版本是1。- 每个条目独立判断:一份已过期的 Cookie 不会拖住其余的,响应会说明存了哪些、跳过了哪些、为什么。
proxy_id会把恢复出来的每个身份都绑到同一个出口。- 条目里的
note会随身份一起恢复。本版本会拒绝的备注——超过 200 个字符,或者不止一行——会被丢掉,身份照样恢复:身份比贴在它上面的标签值钱。 dry_run可以在不写入的情况下回答上面这些问题。
测试、重置与退休
Section titled “测试、重置与退休”测试(POST /api/v1/admin/identities/{id}/test)会以该身份、经由它自己的代理发出一次真实的签名请求,并对回答分类。它不占租约、不消耗配额、不记录任何结果——一次会把被测身份冷却掉的探测,会改变你正在读的那个状态。
| 结果 | 含义 |
|---|---|
ok |
平台正常响应了。Cookie 被接受,签名也被读取了。 |
business_error |
算通过。平台正常响应了,只是说这条作品本身不可用——这是关于内容的事实。内置的冒烟链接确实会失效,而这正是它不会被误读成故障的原因。 |
risk_control |
平台拒绝的是这个身份,不是这次请求。它的 Cookie 已经废了或者被标记了。 |
network_error |
根本没有拿到响应:本机与平台之间的代理、DNS 或 TLS 出了问题。换一个出口再测一次是值得的。 |
重置(POST /api/v1/admin/identities/{id}/reset)会清除冷却时间和连续失败计数,并把身份标记为可用。恢复过程是刻意做慢的——一个反复失败的身份通常确实该被观察一阵——所以这是留给自动恢复无从知晓的那种情况的手动开关:失败并不是这个身份的错。 比如连续查了一批错误的 ID,或者代理已经修好了。如果会话本身已经废了,平台会在几次请求内再次拒绝它,它会重新进入冷却。对已退休的身份,重置会被拒绝:Cookie 已被清除,没有会话可以放回轮换,一个看起来能把它救回来的按钮就是在撒谎。
退休(DELETE /api/v1/admin/identities/{id})会设置状态、把原因记到该身份的时间线上,并立刻清空 Cookie 列。统计值值得留下,被丢弃的凭证不值得。此操作不可撤销。已退休的行会在超过 retention.retired_identity_days(默认 90 天)后,由维护任务删除。
代理页面(/proxies)
Section titled “代理页面(/proxies)”代理是可选的。不配代理时,池子里所有身份共用服务器自身的出口地址——对于一个躲在防火墙后、每天几百个请求的个人实例来说通常没问题,但一旦你运行的身份多于几个,这恰恰就是身份池要避免的那种关联。
以下情况该加代理:
- 这台服务器根本连不上该平台——最常见的是中国大陆访问 TikTok。这时请求会以
UPSTREAM_UNREACHABLE失败,只有挂在代理后面的身份能解决,见这台服务器直连不了的平台; - 每个平台运行超过一两个身份——不配代理时它们全都从同一个地址出去,数量越多,这个模式越显眼;
- 你服务器的地址已经被限流或封禁;
- 你希望出口的国家和时区与身份声称的语言环境一致。
如果你只跑一个身份自用而且它工作正常,就别折腾了。一个挂掉或很慢的代理比没有代理代价更大——探测失败时,它后面的每个身份都会被冷却 15 分钟。
可接受的 URL 形式
Section titled “可接受的 URL 形式”单个添加框和批量导入都按行接受以下形式:
host:porthost:port:user:passuser:pass@host:porthttp://user:pass@host:portsocks5://user:pass@host:port协议:http、https、socks5、socks5h。其他都是笔误而不是功能,会以 scheme_not_supported 被拒。行内没有写协议时按 http 处理。端口缺失或越界会报错,而不是套一个默认值。带方括号的 IPv6 字面量可以识别。
注意这里不检查什么:代理可以指向内网或回环地址。对一个自托管部署来说,本机的 SOCKS 监听或局域网网关就是再正常不过的出口。SSRF 白名单管的是你让服务去抓取的 URL,那是完全不同的另一件事。
代理 URL 里带着账号和密码,所以它以 AES-GCM 密文存储并与行 id 绑定,离开服务时只以掩码形式出现(http://***:***@host:port)。没有「把代理密码显示给我看」这样的接口,将来也不会有。
加一个,或者加一百个
Section titled “加一个,或者加一百个”添加代理接收一个 URL,以及可选的名称、国家和时区;保存前控制台会预览这行被读成了什么(掩码后)。
批量导入接收一整段粘贴内容,最多 500 行。空行和 # 注释会被跳过。允许部分成功正是它的意义所在——一份有三行不合法的供应商清单,其余九十七行照样导入——响应会告诉你每一行的结果,被拒的行会被引用出来,其中任何凭证部分都已剥离:
| 拒绝代码 | 含义 |
|---|---|
unparsable |
无法识别的代理格式。 |
empty |
空行。 |
scheme_not_supported |
不支持的协议。 |
malformed_authority |
主机与端口无法解析。 |
missing_host |
缺少主机。 |
missing_port |
缺少端口。 |
port_not_a_number |
端口不是数字。 |
port_out_of_range |
端口不在 1 到 65535 之间。 |
可选的名称会应用到本批的每一个代理。导入成功的会立刻被探测,方式和下文的全部测试一样;对话框不会等探测跑完。
探测、健康与地理位置
Section titled “探测、健康与地理位置”测试(POST /api/v1/admin/proxies/{id}/test)会经由该代理发出一个很小的 JSON 请求,并报告延迟、出口地址、国家和时区。每次探测都用一个全新的 HTTP 客户端:连接池是与出口绑定的,跨代理复用只会报告池中那条连接所属出口的健康状况。
测试选中的和全部测试会对多个代理跑同样的探测,每次四个并行,进度显示在按钮上(「正在测试 4/15」),结束时给出一条汇总(「12 个正常,3 个失败」)。每次探测的预算是 90 秒,以前一个一个地测,二十个死代理要半小时才报完,而且每失败一个就弹一条提示,把页面淹掉;现在每个代理的结果写在各自的行上,只有汇总值得打断你。
socks5:// 和 socks5h:// 代理的探测方式与 http:// 完全相同。早先的版本根本探测不了它们:测试返回 INTERNAL,后台扫描也给不出任何结论。
探测失败时,失败原因会(去掉凭据后)记在这个代理上,显示在不可用徽标下方,直到有一次探测通过为止;列表响应里对应的字段是 last_error。如果这里写的是 ProxyError: 400 Bad Request,说明这个地址有应答,但它不是正向代理——见排障条目。
后台的 worker 会:
- 每五分钟扫一遍所有代理,回写健康状态和 GeoIP(同一个代理不会在一分钟内被重复探测);
- 当五分钟内某个代理后面的身份累计出现三次
network_error时,立刻探测它,而不是等下一轮扫描——那意味着在一个死掉的出口上再烧五分钟身份; - 把探测失败的代理后面所有可用或已降级的身份冷却 15 分钟。它不会退休它们,也不会搬走它们:Cookie 是好的,坏的只是出口;
- 发出
proxy_unhealthy通知,带上备注(没有备注则带掩码后的 URL)。
一个永久死掉的代理,其身份要不要退休,由你决定——那是一个决策,不是超时的副作用。
国家和时区之所以重要,只有一个具体原因:在这个代理后面铸造时,它们会作为地理提示交给浏览器,让铸出来的身份的时钟和语言环境与它的出口地址一致。探测会自动填写它们;GeoIP 查错时你可以手动覆盖。修改它们不会影响已经铸造出来的身份。
表格的列是:状态、名称、地址(掩码)、出口地区(国家在上、时区在下)、延迟、身份数、最近检查,以及默认隐藏的代理 ID 和创建时间。其中两列需要说明一下:
- 延迟来自你本次会话中在这个页面上跑过的探测。列表本身不携带存储的延迟值,所以没测过的行显示
—。 - 身份数是绑定在这个代理上、尚未退休的身份数量,所有平台合计。如果你本想让身份走这个代理、请求却仍在直连,先看这里是不是
0:身份只在铸造或导入的那一刻绑定代理,之后不会再补。要看某个代理后面挂着哪些身份,打开它的抽屉(见下),或者用身份页面:那里的代理列会显示名称,搜索框也能匹配代理 id。
点击一行会打开这个代理的抽屉:完整的最近一次错误(表格里那一列最多显示两行)、有探测结果时的结果、绑定在它上面的身份(GET /api/v1/admin/proxies/{id};不含已退休的,每个都链接到对应身份,不参与轮换的会标出来),以及过去 24 小时经由它发出的请求,查看请求链接到 /logs?tab=requests&proxy_id=。最后这部分,是某个平台开始拒绝一个出口时的证据:B 站的 412 是对一个地址的判决,而同一个代理后面的所有身份共用这个地址。测试、编辑和删除在抽屉底部。这个页面上的每个写操作都需要 operator 角色,所以低于这个角色时,添加、批量导入、测试、编辑、删除和勾选行都会被隐藏;列表和抽屉照常可读。
你也可以手动覆盖健康状态(对选中行执行标记为可用 / 标记为不可用,或用 PUT /api/v1/admin/proxies/{id} 的 healthy 字段),适用于一个你确定没问题却探测失败的出口,或者相反的情况。下一次探测会覆盖你的判断。
删除一个代理
Section titled “删除一个代理”删除代理会把绑定在它上面的每个身份退休,并清除它们的 Cookie。这不是代码可以放宽的安全余量:身份和它铸造时所在的出口绑定,因此代理没了的身份也就没有将来——留着它,最终意味着让它的 Cookie 从另一个地址发出去。确认对话框会告诉你有多少身份会一起消失,并逐个列出代理及各自的身份数。这个数大于零时,需要输入一段文字来确认:代理的名称,没有名称时是它的主机名——同时删除多个代理时,则是即将被退休的身份数。
如果代理只是暂时不通,什么都别做:探测器会冷却它的身份,代理恢复时它们也会回来。
身份与代理是怎么配对的
Section titled “身份与代理是怎么配对的”- 自动铸造任务会挑一个健康的、且该平台还没有存活身份占用的代理。因此,健康且空闲的代理数量就是自动补充在每个平台上能加到多少个身份的上限;所有代理都被占用时,它报告
no_free_proxy并等待,而不是叠加。退休一个身份会重新释放它的代理。 - 你显式指定的代理(铸造对话框、CLI 的
--proxy,或 API 的proxy_id)会覆盖这个搜索:出口是你有意选的,否则只有一个代理的安装将永远铸不出第二个身份。 - 完全没有配置代理时,身份在直连出口上铸造。这就是一个没有代理的安装所要求的。在铸造对话框里不选代理,走的是和自动任务一样的搜索,所以只有在这种情况下铸造才会直连。
- 导入的 Cookie 使用你在导入对话框里选的代理。默认选项**不用代理(从这台服务器直连)**就是字面意思,而且对这个身份终身有效;导入时不会自动挑代理。
- 系统中没有任何机制会把一个身份挪到另一个代理上。
这台服务器直连不了的平台
Section titled “这台服务器直连不了的平台”在中国大陆,不走代理是连不上 TikTok 的。这时每个请求都会在平台应答之前就在网络层失败,API 报 UPSTREAM_UNREACHABLE,且 details.proxied 为 false。事后再加代理,本身什么也改变不了:池子里已有的身份在铸造或导入时就绑定了直连出口,而这个绑定是永久的。
- 添加一个容器能访问到的正向代理——本机 Clash 或 mihomo 的入站端口,而不是 VPN 订阅链接——然后点测试,直到它变成可用。
- 把该平台直连的身份退休掉。
- 在这个代理后面铸造,或者导入时在对话框里选上这个代理。
- 确认这个代理的身份数一列把它们算进去了。
如果只有一个代理,在铸造对话框里显式选中它:自动补充只会取该平台还没有身份在用的代理,所以铸出第一个之后就停了。该用什么地址、过程中会遇到哪些错误,见排障文档。
调度器页面(/scheduler)
Section titled “调度器页面(/scheduler)”这个页面把调度器的配置项放在它们所作用的身份池旁边,而不是埋进一个按字母排序的设置列表里。它显示:
-
三个指标块——当前可被调度、处于
cooling和处于degraded的身份各有多少。可被调度的数量是active减去保留身份(GET /api/v1/system/status里的reserved_active):保留身份是可用的,但只有指名它的请求才能用上,所以它不算容量。下面每一个数字都是对这份实时统计的表述,脱离它去读这些数字,正是把低水位线设成一个池子从没接近过的值的原因。 -
按平台——同一份统计按平台各占一行,可调度、保留、冷却中和已降级并排显示。请求只会由同平台的身份来发,所以跨平台求和的数字可能看着健康,而其中某个平台已经无身份可用。
-
登录态与接口覆盖——同一个身份池,按接口来数,因为调度器就是按接口分配身份的。限定在作者作品上的登录身份在池子里,却不出现在其他任何接口上;保留的身份哪个接口上都没有;一个需要登录的接口如果轮换里一个登录身份都没有,之后每一页都会失败,而池子看上去是满的——这些按平台的统计都看不出来。每个平台一块,包括:
- 不需要登录的接口游客优先开关,也就是
pool.<platform>.guests_first(见哪些内容需要登录身份),下面一行字说明当前这个位置意味着什么; - 有多少个身份只服务点名它的请求;
- 一张表,轮换能用到的每个接口一行。登录要求是轮换卡片清单里用的那个标记。可分配是轮换可以派到这个接口的可用和降级身份数——在所有接口上参与轮换的,或者清单里列了它的;接口从第一页起就需要登录时只算登录态——并链接到正好这些身份,
/identities?platform=<平台>&serves=<接口>&ready=1。其中登录态是其中登录身份的个数;接口本身或它的翻页需要登录时,0 会用警示色显示,因为那些页随后会返回IDENTITY_LOGIN_REQUIRED。专门限定是其中清单里列了这个接口的个数,冷却中是正在休息、恢复后会回到这个接口上的身份,同样带链接,用的是state=cooling。
只回答发起请求的身份本人数据的接口不在表里:它们总是需要
identity=,而卡片上的任何东西都限制不了点名身份的请求。这张卡片读的是GET /api/v1/admin/identities/coverage;viewer 及以上角色能看到,demo 账号看不到,因为这个接口拒绝它。修改开关需要 operator 角色。 - 不需要登录的接口游客优先开关,也就是
-
一次请求是怎么挑身份的——按顺序的五道关卡:
- 接口熔断。 至少要有
sched.circuit_min_identities个不同身份同时失败才会触发,所以一个坏身份不会把接口对所有人关掉。熔断期间每个间隔只放行一次探测,其余请求直接拒绝,不消耗任何配额。 - 排序——先按健康档位,再按最久未使用,最后按每次调用生成的抖动。
- 在途——每个身份同时只能有一个请求在途。
- 配额——按(身份,接口)各自一个令牌桶。
- 结果回写——请求结果会折回身份的状态。
- 接口熔断。 至少要有
-
一段关于公平性的实话。 排序的目标是「没有哪个身份被系统性偏袒」,而不是严格轮询。与均匀随机基线相比,实测得到的分散度并不优于随机。调度器真正保证、并且被测试严格断言的,是互斥、配额,以及没有身份被饿死。不要把这个轮换理解成轮流坐庄。
-
每一项
sched.*和pool.*配置的编辑器(按平台的那些除外——游客优先开关在覆盖卡片上,水位线是下面那张表),以及一个折叠区,说明其中有多少项是存在数据库里、而不是来自默认值或环境变量种子。 -
各平台身份池大小——六个
pool.<platform>.min_size和target_size配置合成一张表格,行是平台,列是低水位和目标数量,而不是夹在全局水位线中间的六行-1——那样-1看上去像是谁特意选的值。留空的格子表示沿用全局值,并把沿用的那个值显示在格子里;清空一个格子写入的是-1,而不是删除这条覆盖值,因为删除会回落到环境变量种子,而这个页面看不到有没有设种子。整张表只有一个保存按钮,一起写入所有改动,这样目标数量不会在两次点击之间低于它自己的下限——不过写入仍然是一条接一条,并不是原子的。
写入需要 operator 角色。demo 账号能看到统计和轮换说明,看不到配置项和覆盖卡片,因为这两个接口都不对它开放。
按接口的健康状况和熔断状态不在这个页面上——它们在总览页(/),那里会列出所有已声明的接口,无论有没有流量。见控制台总览。
熔断、令牌桶,以及熔断打开时该怎么办
Section titled “熔断、令牌桶,以及熔断打开时该怎么办”令牌桶。 配额的键是 (身份, 接口),而不是只有身份。没有接口这一维时,一个身份可以把全部预算花在最敏感的那一个调用上,这读起来就是「这个访客只做一件事,而且一直在做」——比在多个接口上均匀分布更刺眼。令牌桶按流逝时间惰性回填,闲置的桶 24 小时后过期。
| 接口 | 突发量(桶容量) | 每秒回填 | 风险权重 |
|---|---|---|---|
douyin.content_detail、tiktok.content_detail |
5 | 0.30 | 1.0 |
douyin.author_profile、tiktok.author_profile |
4 | 0.20 | 1.2 |
douyin.related_posts、douyin.live_room、douyin.live_room_by_id 及对应的 TikTok 接口 |
4 | 0.20 | 1.2 |
douyin.music_detail、douyin.hashtag_detail、douyin.live_status、douyin.trending 及对应的 TikTok 接口 |
4 | 0.20 | 1.0 |
douyin.comments、douyin.comment_replies、douyin.mix_posts、douyin.author_mixes、douyin.music_posts、对应的 TikTok 接口及 tiktok.hashtag_posts |
3 | 0.15 | 1.5 |
douyin.live_ranking |
3 | 0.15 | 1.2 |
douyin.author_posts、douyin.author_likes 及对应的 TikTok 接口 |
3 | 0.12 | 1.8 |
tiktok.author_followers、tiktok.author_following |
3 | 0.12 | 1.8 |
tiktok.search_posts、tiktok.search_authors |
2 | 0.08 | 1.8 |
douyin.session_check、tiktok.session_check、bilibili.session_check |
3 | 0.20 | 1.0 |
bilibili.content_detail、bilibili.content_streams |
5 | 0.30 | 1.0 |
bilibili.author_profile |
4 | 0.20 | 1.0 |
bilibili.related_posts、bilibili.live_room |
4 | 0.20 | 1.2 |
bilibili.trending、bilibili.live_streams |
4 | 0.20 | 1.0 |
bilibili.comments、bilibili.comment_replies、bilibili.author_collections、bilibili.collection_posts、bilibili.author_mixes、bilibili.mix_posts |
3 | 0.15 | 1.2 |
bilibili.author_posts |
2 | 0.08 | 1.8 |
bilibili.search_posts、bilibili.search_authors |
2 | 0.08 | 1.5 |
未登记的接口(default) |
3 | 0.15 | 1.5 |
把回填速率读成「每个身份每 1/速率 秒一个请求」:0.12/s 大约是单个身份每八秒一次列表类调用,无论排了多少请求。风险权重会在该接口命中风控后乘进冷却时长。
熔断。 单个身份被限流是常事。所有身份在同一个接口上都失败则是另一回事——通常是上游接口变了或者签名器死了——这时继续重试只会烧池子。当以下三个条件在 300 秒的滚动窗口里同时成立时,接口熔断:
| 条件 | 配置项 | 默认值 |
|---|---|---|
| 样本足够多 | sched.circuit_min_samples |
20 |
| 风控率高于阈值 | sched.circuit_risk_threshold |
0.6 |
| 失败跨越足够多的不同身份 | sched.circuit_min_identities |
3 |
第三个条件是把「接口坏了」和「一个身份坏了」区分开的关键;没有它,一个反复抽风的身份就能把接口对所有人熔断。
熔断打开后会持续 300 秒,其间每 60 秒放行恰好一次探测(这两个值都没有开放为配置项)。调用方会立即收到 503 ENDPOINT_CIRCUIT_OPEN 和一个 retry_after——等待改变不了答案,所以调度器不会把等待预算花在它身上。探测成功时熔断立即关闭,窗口清空。熔断打开还会触发一条 endpoint_circuit_open 通知。
熔断打开时该怎么办,按值得尝试的顺序:
| 先看什么 | 如果是 | 那么 |
|---|---|---|
| 是一个接口还是全部?(总览页) | 只有一个接口 | 那条路径的签名或上游结构变了,或者那条路径受签名保护而你的身份没有对应的 visitor id。用调试台对着那个接口试——见调试台与工具。 |
| 某个平台的所有接口 | 问题在身份池而不在接口。看会话凭据列:满屏的 missing 或 too_short 就是会话已经用尽。 |
|
| 身份的会话凭据列 | 大多是 ok |
Cookie 没问题;怀疑签名或出口。先探测一个身份,再探测它的代理。 |
大多是 missing / too_short |
退休它们,让池子铸造替补。 | |
| 单个身份的抽屉 | 一个接口拒绝,其他正常 | 不是 Cookie 废了。是签名,或者受签名保护的路径。 |
| 全部都拒绝 | 这个身份用尽了。退休它。 | |
| 代理页面 | 某个代理刚变成不可用 | 它的身份已被自动冷却。修好或删掉这个代理;在它恢复之前不要去重置那些身份。 |
这里没有「重置这个熔断」的按钮,也没有对应的 API 接口,这是设计使然:熔断的接口会在一次探测成功后自行恢复,而在病因未除之前强行放开它,只会让池子烧得更快。病因修好之后,常规做法就是等下一次探测放行——最多一分钟。如果一分钟都嫌久,还有一个手动的应急口子:直接删掉这个熔断在 Redis 里的两个键,具体命令见故障排查。那是一个刻意留在控制台之外的动作,不是缺了一个按钮。
接口访问控制页面(/endpoint-access)
Section titled “接口访问控制页面(/endpoint-access)”这个页面之所以属于本文,只有一个理由:每一个开放的接口都在消耗你的身份池。 匿名调用者不会自带身份。
默认情况下所有接口都需要 API Key 或控制台会话,这是正确的默认值——一台放在公网服务器上的实例是陌生人能够到的机器,而它存在的全部意义就是花掉别人的身份池。这个页面按标签分组列出所有已文档化的操作,数据由 API 文档本身生成,所以开关和它写入的配置项不会对「这条路径叫什么」产生分歧。
- 开关的语义是**「该接口需要 API Key」,所以打开是受保护状态,一整页都打开的开关描述的是一个什么都没暴露的实例。每一行都会标出自己处于哪一边——需要 Key 或 无需 Key。把某个开关关掉**才是去掉鉴权检查,因此需要确认的是这个方向;重新打开则不需要。
- 开放某个接口会写入
api.public_endpoints,这是一个 SENSITIVE 配置项(写入时会带上显式的确认标记),内容是形如"<METHOD> <路径模板>"的字符串列表,与 API 文档里的写法完全一致。正因为它是 SENSITIVE,只有管理员能改:低于这个角色时开关会被隐藏,并有一行字说明原因。 - 每个开放的接口都会写明它服务哪些平台,在列出它们的告警卡片里和确认框里都有。路径里带
{platform}的路由服务本版本支持的所有平台,以后新增的平台也包括在内——5.1.6 之前开放的接口,在 B 站上线的那一天就开始消耗 B 站的身份池,而这个页面当时一个字都没提。告警卡片上还有全部关闭,会先确认再执行。 - 管理、认证与初始化类路径永远无法开放。
/api/v1/admin、/api/v1/auth和/api/setup无论配置怎么写都会被服务端拒绝,所以这些行渲染成锁定状态、不提供开关——一个会撒谎的开关比没有开关更糟。 - 有少数几条路由在代码里根本没有鉴权检查(你没法先登录再去访问登录接口)。它们被单独列为「无需凭据」,不计入告警,这里也没有开关能关掉它们。它们都不抓取任何内容,因此也都不消耗身份池。
- 匿名调用者只获得读权限(
douyin:read、tiktok:read、bilibili:read),绝不包含管理与写入,并且按来源地址而不是按 Key 限流,使用api.default_rate_limit_per_min(默认 120)。在 Docker 的用户态代理后面,所有调用者可能看起来都来自网桥网关,这会退化成一个共享的计数桶——是更严,不是更松。
关于这个页面的完整说明——哪些路径永远无法开放、为什么有 Key 是必要但不充分的、以及那些从来就没关过的路由——见用户与 API 密钥。在可从公网访问的实例上开放任何接口之前,先读安全。
我需要多少个身份
Section titled “我需要多少个身份”从令牌桶算起,不要凭感觉。一个身份在每个接口上分别大约能维持 refill_per_sec 个请求每秒,同时整体上最多只有一个请求在途。
| 你要做什么 | 接口 | 单身份能力 | 要达到 1 请求/秒需要 |
|---|---|---|---|
| 查单个视频 | *.content_detail(0.30/s) |
约 18 次/分钟 | 约 4 个身份 |
| 读用户资料 | *.author_profile(0.20/s) |
约 12 次/分钟 | 约 5 个身份 |
| 翻评论 | *.comments(0.15/s) |
约 9 次/分钟 | 约 7 个身份 |
| 遍历作者作品 | *.author_posts(0.12/s) |
约 7 次/分钟 | 约 9 个身份 |
B 站的速率与此相同,只有 UP 主投稿列表 bilibili.author_posts 是 0.08/s:约 5 次/分钟,要达到 1 请求/秒约需 13 个身份。
然后再留出余量,因为池子并不总是全员在岗:
- 冷却中的身份不可被调度。池子健康时这只是一小部分;出事时那就是大部分。
- 补充任务只统计连续失败次数低于
pool.max_fail_streak(默认 3)的身份,所以一个烧掉的身份不会白占名额。 - 每次铸造都是一个一个来、每个以秒计,所以池子在损失之后没法快速长回来。把
pool.target_size设得比实际需要高几个,比在压力下临时铸造便宜。
默认值——每个平台低于 3 补充、补到 8——适合一个每天做几千次详情查询的个人实例。三个身份、不配代理、每六小时跑一次关注列表,是一个能工作的配置;三个身份去支撑一个公开接口则不是。
拿不到租约的请求最多等 sched.max_wait_seconds(默认 10)秒,然后以 503 IDENTITY_POOL_EXHAUSTED 失败。如果你看到这个错误,只有三个选择:加身份、减调用方,或者接受排队。
为什么它们会一起进入冷却
Section titled “为什么它们会一起进入冷却”因为冷却是按身份计的,而几乎所有诱因都是共享的。按可能性从高到低:
| 原因 | 特征 | 怎么办 |
|---|---|---|
| 某个代理探测失败 | 代理页面显示它不可用;被冷却的身份都用着这个代理;冷却时长清一色 15 分钟 | 修代理。身份会自己回来。不要先去重置它们——它们会立刻再次冷却。 |
| 平台正在拒绝这个部署 | 冷却身份横跨多个代理;紧接着某个接口熔断 | 停止发送流量并等待。往里面铸造只会让信号更响,这也是补充任务刻意拒绝这么做的原因。 |
| 会话确实已经用尽 | 会话凭据列满是 missing / too_short;抽屉里显示所有接口都拒绝 |
退休它们,让池子铸造替补,或者导入新的 Cookie。 |
| 签名或上游发生变化 | 会话凭据列大多是 ok,但每个请求都被判为 risk_control;通常先是一个接口,然后是全部 |
检查浏览器容器和接口健康看板;这不是身份池的问题。 |
| 分类器把正常回答读成了风控 | 探测能过、平台明显没事,身份却在被冷却 | 重置受影响的身份。这真的发生过:2026-09-09 之前,本程序把抖音对「作品不存在」的回答读成了风控,所以查一个错误的 ID 就会冷却发起查询的那个身份。修好分类器并不能修复它已经造成的伤害——而这正是重置的用途。 |
| 到平台的网络有问题 | 出现的是 network_error 而不是 risk_control;没有冷却,但连续失败次数在涨 |
查 DNS、TLS 和代理。network_error 会退还令牌,且本身从不冷却身份。 |
从命令行操作
Section titled “从命令行操作”CLI 位于 api 容器内,读取同一套数据库和配置:
docker compose -p dtk -f docker/compose.yml exec api dtk identity list --platform douyin| 命令 | 作用 |
|---|---|
dtk identity list [--platform P] [--state S] [--limit N] |
列出身份及其状态、浏览器、代理、连续失败次数、冷却和最近使用时间。Cookie 从不出现。 |
dtk identity mint --platform P [--count N] [--proxy ID] |
铸造 1 到 20 个:B 站走普通 HTTP,抖音和 TikTok 走 browser-rpc。对后两者,未设置 DTK_BROWSER_RPC_URL 时会给出可操作的失败提示。 |
dtk identity test <id> [--url URL] [--timeout S] |
一次真实的签名请求。不占租约、不消耗配额、不记录结果。 |
dtk identity retire <id> [--reason TEXT] |
退休并清除 Cookie。重复退休会被拒绝,而不是覆盖掉原来的退休原因。 |
dtk proxy list [--healthy] |
列出代理,URL 掩码,含地理位置和探测状态。没探测过的代理显示 unchecked 而不是 healthy。 |
dtk proxy add <url> [--label L] [--country C] [--timezone Z] |
添加一个;URL 在写入数据库之前就已加密。 |
dtk proxy import <file> |
每行一个 URL,后面可以跟空白和一个备注;跳过 # 注释;跳过重复项。被拒的行只报行号,绝不回显内容。 |
dtk proxy test <id> [--probe-url URL] [--write/--no-write] |
探测一个代理,默认把健康状态和 GeoIP 写回该行。 |
只要不产生歧义,id 可以用表格里打印的短前缀。完整参考见命令行参考。
影响身份池的配置项
Section titled “影响身份池的配置项”下面这些配置项里,pool.* 和 sched.* 在调度器页面上编辑,其余的在设置页面上(api.public_endpoints 另有上文说的接口访问控制页面)。它们全都可以用 dtk config 修改,也都与其余全部配置一起记录在配置参考中。
| 配置项 | 默认值 | 作用 |
|---|---|---|
pool.min_size |
3 | 每个平台的低水位线。低于它,补充任务开始铸造。 |
pool.target_size |
8 | 补到多少。设得比 min_size 低时会被抬到 min_size。 |
pool.<platform>.min_size |
-1 | 单个平台(douyin、tiktok 或 bilibili)自己的下限。-1 沿用 pool.min_size;0 关闭这个平台的自动铸造和身份池告警。 |
pool.<platform>.target_size |
-1 | 单个平台自己的目标值。-1 沿用 pool.target_size。 |
pool.<platform>.guests_first |
true | 这个平台不需要登录的接口先分配给游客身份,只有没有游客能接下请求时才用登录身份——轮换里没有游客服务它们,或者游客全都在忙、配额用完。需要登录的接口不受影响。关掉后登录身份和游客按最近使用时间平摊这些接口。每个请求都会读取。 |
pool.max_fail_streak |
3 | 连续失败达到该值后,身份不再计入池子水位,补充任务会去补一个新的而不是把它算进来。 |
pool.health_prior |
0.8 | 流量太少无法评分时,假定的成功率。 |
sched.max_wait_seconds |
10 | 一个请求在失败前最多等多久拿身份。 |
sched.cooldown_base_seconds |
60 | 命中风控后的首次冷却时长。 |
sched.cooldown_max_seconds |
21600 | 冷却上限;达到它意味着身份被标记为降级。 |
sched.circuit_risk_threshold |
0.6 | 接口可被熔断的风控率阈值。 |
sched.circuit_min_samples |
20 | 熔断前在 300 秒窗口内所需的最少样本数。 |
sched.circuit_min_identities |
3 | 触发熔断所需的不同失败身份数。 |
sched.queue_max |
500 | 最大排队请求数。 |
retention.identity_events_days |
90 | 身份时间线保留多久。 |
retention.retired_identity_days |
90 | 已退休身份的行在被删除前保留多久。 |
api.public_endpoints |
[] |
无需 Key 即可访问的接口。SENSITIVE。 |
api.default_rate_limit_per_min |
120 | 单 Key 与单匿名地址的限流上限。 |