歌单收藏 405:请求身份与时序的深入调查
2026-10-17 的调查确认了一个具体拒绝条件:**在本次成功的官方网页请求中,只增加 os=iphone Cookie 就会得到 405;去掉它,原样请求又成功。**这证明客户端身份标记可以触发相同的“操作过于频繁”正文。
**23:00 接续结论:**公开模块改用官网的 weapi 网页身份后,在新登录会话、从未操作过的目标上仍然返回 405,说明还存在独立的频率/路径级拒绝。随后经用户同意,在本机用 mitmproxy 抓取官方 Mac 客户端的收藏请求:客户端每次收藏前约 0.1 秒向易盾申请一个新的 170 字符令牌,并同时放在请求体 checkToken、加密头 X-antiCheatToken 和 HTTP 头中;取消收藏不带该令牌。SDK 不内置令牌生成器;10-18 的接续实验改为由调用方提供令牌,见文末。本轮没有尝试生成、代换或重放该令牌,也没有为绕过限制继续实验。
但这还不是完整根因。weapi + os=web 的 SDK 探针曾成功,落到公开模块后却再次失败;稍后,先前成功的官方请求重放也被拒绝。当前证据同时包含请求字段差异和随时间变化的响应,不能把所有 405 归为同一个原因。候选修复已撤回,#77 保持未解决,正式窗口 0/3。
**17:48–18:04 的接续调查:**新生成的官方界面请求也返回了 405,但响应报告的来源地址由旧成功请求的 IPv4 变为 IPv6。只读检查进一步查明:curl 连接的是经本机 TUN 路由的合成地址,-6 还是该 IPv4 地址的映射形式,不能据此固定远端公网出口。一次预登记写入也未得到预期的旧出口。现在既不能把剩余失败只归为旧密文重放,也不能声称已经排除出口变化。详见新官方请求与出口对照。
一手资料与实际请求
NMTID 的处理确有差异,但不是现成答案
本地 processCookieObject 在非登录请求中无条件把 NMTID 换成随机值;eapi 的加密头与 HTTP Cookie 白名单又不包含该字段。实际浏览器出站 Cookie 则保留了用户提供的值。两边 MUSIC_U、__csrf 相同,不代表完整请求身份相同。
上游 PR #243 的三个修复分别处理了服务器值采集、调用方值优先、eapi 实际发送遗漏,没有提供收藏 405 恢复证据。更早的 Binaryify #1788 确有取消随机 NMTID 后验证码登录恢复的报告和维护者确认,但那是登录路径。本票仅补入已有 NMTID 的实验仍然 405,不能直接把上游修正当成本票的答案。
设备状态、Watchman 状态和固定标记不是同一种东西
官方设备脚本 调用 createNEFingerprint().getToken(),再请求 /api/middle/device-info/web/get。本次从 Chrome 的 responseReceivedExtraInfo 中确认实际 /weapi/middle/device-info/web/get 响应通过 Set-Cookie 下发 sDeviceId,再从后续 requestWillBeSentExtraInfo 确认它确实被发送。
官方 Watchman SDK 自己读写 WM_TID、WM_NI、WM_NIKE;其存储处理器从响应取得状态,WM_NIKE 的计算涉及 WM_NI。所查脚本没有 NMTID、sDeviceId 的直接推导关系,不能把这些字段混称为一个 token。
官方 injector 对命中域名的 XHR/fetch 写入固定 Nm-GCore-Status: 1,并没有在这条路径上计算请求签名。官方网页 core 的收藏 handler 只提交 id,协议层补充 CSRF 并加密。16:50 的成功收藏实际携带 sDeviceId、三个 WM Cookie 和该固定头,但没有观察到 X-antiCheatToken。
**共现不等于必要。**16:56 的 SDK 探针只使用用户原始 Cookie,没有浏览器新取得的 sDeviceId、WM Cookie,也成功过。因此现有证据不支持要求调用方补齐所有这些字段,更不支持生成静态假 token。
默认伪装 IP 的名称与实际网段不符
generateRandomChineseIP 的前缀表并非全部来自中国网络。本次保存的随机伪装值,不是用户实际出口 IP,在 APNIC RDAP 中分别对应:
| 探针 | 随机地址 | 登记国家/网络 |
|---|---|---|
| 16:56 成功 SDK 探针 | 116.90.50.192 | AU/DIGITALPACIFIC |
| 17:04 失败公开模块 | 116.88.29.192 | SG/SGCABLEVISION-SG |
| 16:48 NMTID 探针 | 116.83.56.139 | GB/UK-COLT-20070514 |
RIR 登记国家不是网易云采用的地理定位或风控结论,成功样本也并非中国登记地址。对使用 os=web 的失败请求仅去掉转发 IP 头仍然 405。因此这里确认了默认 IP 生成器的问题,没有确认它是本票根因,本 PR 不顺带修改全局 IP 策略。
从成功请求出发的对照
环境、账号、目标和完整列表核对方法沿用首阶段记录。源码基线为 c832da2,候选另有精确源码快照。以下时间均为北京时间。成功后均通过无缓存列表确认目标出现、取消后消失及完整状态恢复;原始请求、Cookie 和账号快照只保存在本机。
| 时间 | 变化/对照 | 原始 HTTP/业务结果 |
|---|---|---|
| 16:48:51.814 | 当前 iPhone/eapi 仅补入用户 NMTID,镜像到 Cookie 和加密 header | 200/405,1 次写即停 |
| 16:50:49.698、16:51:50.183 | 官方网页真实界面收藏、取消;捕获收藏的完整出站头 | 均 200/200 |
| 16:54:19.642、16:54:19.854 | 官方收藏的正文、实际 Cookie 和请求头保持原样,换由 Bun fetch 发送;取消按下述恢复方式构造 | 收藏、取消均 200/200 |
| 16:55:04.881 | 在上一成功收藏请求中只追加 os=iphone Cookie | 200/405,停止;列表未变 |
| 16:55:41.101、16:55:41.298 | 去掉 os,恢复同一官方原样收藏请求;随后构造取消恢复请求 | 收藏、取消均 200/200 |
| 16:56:59.967、16:57:00.162 | SDK weapi 请求,仅在 fetcher 中把最终 Cookie 的 os=iphone 改为 os=web | 收藏、取消均 200/200 |
| 17:02:39.453 | 候选 A:公开模块固定 weapi/web 标记,同时换网页 UA、撤去 iPhone 版本覆盖 | 200/405,正式首窗 0/3,停止 |
| 17:04:31.084 | 候选 B:撤回额外 UA/版本变化,公开模块固定 weapi/web 标记,保持此前 iPhone UA/版本模板 | 200/405,1 次写即停 |
| 17:07:45.219 | 冻结候选 B 失败请求,只去掉转发 IP 头 | 200/405,1 次写即停 |
| 17:09:08.005 | 再重放 16:50 捕获、16:54 和 16:55 曾成功的官方原样收藏请求 | 200/405,停止全部后续账号写入 |
上述 405 均为 HTTP 200,正文与首阶段相同,包含 code: 405、message/msg: 操作过于频繁,请稍后再试,其余三个数据字段为 null。成功正文均为 { "code": 200 }。SDK 失败仍保留正文并补入既有 retryAfter: 30;原始 fetch 重放不经过 SDK 错误映射。
Bun 对照中的取消恢复使用已捕获的官方取消 URL/加密正文,Cookie 沿用官方收藏请求的实际值,并调整 Referer 与正文长度;由于未取得官方取消的 ExtraInfo,不能称为完整取消请求的逐字节重放。收藏重放保持实际业务头和正文,HTTP/2 的冒号伪头由 Bun 根据 URL/方法重新生成,传输编码也由运行时处理。
各组写入前记录目标、变更、上限和失败停止条件。下表为“写请求数/全部请求数”;全部请求数只统计对应探针,不包含浏览器资源和遥测。
| 组别 | 预设上限 | 实际 |
|---|---|---|
| NMTID | 4/16 | 1/6 |
| 原样 Bun 对照 | 2/10 | 2/6 |
| 只加 os | 2/10 | 1/5 |
| 去掉 os 的反向对照 | 2/10 | 2/6 |
| SDK web 标记探针 | 2/10 | 2/6 |
| 候选 A 正式窗口 | 6/24 | 1/6 |
| 候选 B 一对验证 | 2/10 | 1/5 |
| 去 IP | 2/8 | 1/4 |
| 最后状态对照 | 2/10 | 1/5 |
官方界面对照预设最多 2 次写入,实际 2 次;辅助 SDK 列表检查另有 3 组、每组 2 个只读请求。**本轮共 14 个收藏/取消写请求;加上首阶段 6 个,共 20 个。**所有失败尝试保留,没有自动重试,没有临时歌单,最终完整列表仍与原始基线一致。诊断操作本身也可能影响上游状态,不能认为每组开始时上游内部状态都相同。
16:43 的浏览器初始化捕获到 47 个站点请求,其中无收藏/取消请求;页面自动进行了设备登记。16:50 的完整网络观察器捕获 23 个请求,其中 1 个收藏请求。观察器在取消前结束,取消的 200 正文来自另一份请求详情;没有捕获该次取消的 ExtraInfo Cookie 头。界面取消距收藏约 60 秒,不计入稳定性轮次。
哪些结论有证据,哪些还没有
- **Bun 本身不是充分致因。**官方完整请求在 Bun 中成功过;不能把故障归结为“必须从浏览器发送”。
- **
os=iphone是已验证的拒绝条件之一。**同正文、Cookie、头和运行时,只追加这个字段就从 200 变为 405,去掉又恢复。它解释了固定平台身份为什么不能凭一次成功就宣称正确,但不证明所有 iPhone/eapi 请求都会失败。 - **固定字段方案仍不足以解释所有结果。**16:56 的 SDK 成功探针与候选 B 的失败请求,其 URL、UA、Cookie 名集合及固定值一致;剩余差异包括随机 IP、
NMTID、_ntes_nnid、_ntes_nuid、WNMCID、deviceId和加密随机数。尚未逐个验证这些差异的因果关系。 - **成功请求后来也失败,限制了纯静态归因。**17:09 使用的是重放请求,未再发起新的官方界面收藏。可能涉及上游状态变化、请求重放时效或其他上下文;不能据此声称账号已被全局封禁、官方新请求也必然失败,或等待某个固定时间必定恢复。
候选与稳定性窗口
候选 A 先修改契约测试,观察到旧实现 4 个相关断言失败;改为网页 UA/weapi/web 标记后,58 个专项测试和类型检查通过。17:02 开始正式窗口:计划 A/B/A 三轮,目标为 2889745179 和 106697785,都已确认属于他人且未收藏;轮间至少 60 秒,上限 6 个正常写请求/24 个总请求,失败停止。
该窗口第 1 轮收藏失败,实际 6 个请求、1 个写入尝试、完成 0/3,后两轮未执行。源码基线 c832da2 加候选 A,模块 SHA-256 为 1aae10c8a45ecabe66e9afa86a8b4037ad97d9038426a87a5827d323f5f80b3b;本机保留与该 hash 相符的源码快照。
候选 B 只保留此前成功探针对应的 weapi/web 标记变化,仍保留原 iPhone UA、appver、osver、channel;专项测试 8 项和类型检查通过,但公开模块一对验证在收藏阶段再次失败。它是诊断组,没有作为新窗口替换前一个失败窗口。
两份候选均未通过真实验证,生产改动与候选测试已撤回,只在本机归档。当前 PR 保持调查草稿。尚无通过的首窗,因此没有可按“首窗完成后至少 24 小时”安排的第二个成功窗口。
截至 17:09,下一步是取得新生成的官方成功请求,再确认其原样 Bun 重放仍成功。下节记录了实际接续结果;没有成功控制时,不继续比较固定字段。
新官方请求与出口对照
计划、现场结果与停止点
本次源码仍为 d7b90a9,独立运行编号为 01a1493d。重新读取当前账号状态,未套用旧基线:53 张歌单、35 张收藏,目标 2889745179 属于他人且未收藏。完整 id/subscribed 列表排序后的摘要与旧记录恰好一致,但恢复依据是本次新快照。
写入前预登记本 Session 最多 10 次收藏/取消请求,其中 2 次只留作必要恢复。初始计划为新官方界面加 Bun 控制最多 4 次、仅替换 NMTID 最多 2 次、分析后的原值反向对照最多 2 次。控制失败即停,不执行字段组;取消只针对本轮新增目标,随后核对完整列表。只读请求另设上限 40 次;浏览器资源与自动请求单独记录。
17:50:34.048,点击官网真实收藏控件生成了新请求,返回 HTTP 200/业务 405,正文与此前拒绝相同。观察器取得了该次请求和响应的 ExtraInfo。官方加密函数的旁路观察只记录字段形状,不改入参或结果:正文键为 id/csrf_token,id 是字符串,CSRF 非空;仍使用此前相同版本的官方 core。无缓存列表确认目标没有出现。因此没有发送原计划的 Bun 成功控制、取消或 NMTID 实验。
与 16:50 成功的官方请求相比:
- URL(含 CSRF query)相同;非 Cookie 请求头仅
content-length不同,加密正文不同。 - Cookie 名集合相同;
MUSIC_U、__csrf、NMTID以及其他 7 个 Cookie 值相同。 - 12 个浏览器状态值不同,包括
sDeviceId、三个 WM Cookie、WNMCID、两个_ntes_*、JSESSIONID-WYYY及统计字段。因此这不是只改变密文的单变量实验。 - 两次响应中的
cdn-user-ip与x-request-ip各自相等,但旧成功请求为公网 IPv4,新失败请求为公网 IPv6,且不是 IPv4 映射地址。原值只留本机。同机器不等于同出口;这两个响应头是现场可观测信号,不是网易云风控规则的说明。 - 两次都没有
Retry-After或 Set-Cookie,不能从响应推导恢复时间,也没有可直接写回以恢复会话的新 Cookie。
随后冻结官网已发出的账号读取请求 /weapi/w/nuser/account/get,只读比较运行时与本地连接地址族;保留 URL、正文、Cookie 和业务头,三次均为 HTTP 200/业务 200:
| 执行方式 | 响应报告的来源 | 与 16:50 成功请求的来源相同 |
|---|---|---|
curl -4 | IPv6 | 否 |
curl -6 | IPv4 | 是 |
| Bun fetch | IPv4 | 是 |
这是实际响应观测,不是把 -4/-6 当成上游出口保证。它也不能证明账号读取成功就代表收藏可用。
在分析这一新差异后,取消未执行的 NMTID 和反向字段组,在原 10 次总预算内预登记一个最多 2 次写入的出口对照:冻结 17:50 官方失败收藏请求,用刚才读取时呈现旧 IPv4 来源的 curl -6 发送;成功才立即取消并核对状态,任一失败停止。恢复预先准备官方取消的 id/pid/csrf_token 正文形状、同一会话 Cookie 和官网界面路径,不依赖旧账号快照。
17:56:34.006,该请求仍返回 HTTP 200/业务 405。**响应实际报告的却仍是新 IPv6 来源,未实现预期的旧 IPv4 路径。**请求正文、Cookie 和非伪头保持不变;HTTP 版本、连接等仍由 curl 管理。这是一次失败且自变量未控制住的实验,不能用它排除出口因素,也不能声称“同出口的新官方请求和 SDK 均失败”。响应 Date 和追踪字段不同于 17:50 的响应,不是取到了同一份旧响应记录。失败后停止全部后续写入,没有改目标或再改字段。
停止写入后,18:03 又做了两组只读检查,各在单个 curl 进程中连续读取两次。两组分别使用 -6 和 -4,四次均为 HTTP 200/业务 200;各组第二次请求的 num_connects=0,本地端口、远端地址、HTTP/2 连接及响应报告的来源保持相同。本次两组响应都报告新 IPv6 来源,没有取得旧 IPv4 来源。
**这里查明了地址族实验失去控制的具体原因:**curl 记录的 IPv4 远端属于 198.18.0.0/15 合成地址,IPv6 远端则是该地址的 IPv4 映射形式;本机路由查询确认二者经过同一个 TUN 接口。curl 报告 proxy_used=0 只能说明没有使用它自身的显式代理,不能排除这个网络层转发。-4/-6 没有固定从 TUN 之后访问网易云的公网来源。连接复用只在上述四次读取中验证,不能保证未来写请求的远端出口;本次没有修改用户网络设置或继续写入。这是实验方法的因果解释,仍不是收藏 405 的服务端根因。
本 Session 实际 2 次收藏写入、0 次取消,目标始终未出现;浏览器之外另有 17 次只读请求,包含准备阶段的登录检查、三个运行时/地址族对照和上述四次连接检查。浏览器观察器记录 376 个请求,其中只有 1 个收藏请求;其他包含资源、查询、设备登记和遥测,不能全部称为只读。17:58:10 最终无缓存列表与本次基线完全相同,此后没有账号业务写入;浏览器及观察器已关闭,没有留存运行中的探针。
此前 20 次调查写入保留,本次之后共 22 次;这个累计口径从 16:05 的 #77 调查开始,不含更早 #33 的验证。正式失败窗口仍为原来的 0/3,两个新诊断组都不计为验收窗口。旧证据日志、失败窗口、候选 A 源码和候选 B 补丁的摘要未变化。新脱敏日志、比较摘要及文本探针归档以本机 _notes/issue-77-01a1493d-* 和 _notes/issue77-01a1493d-* 命名,认证捕获仍只在本机保存。
身份字段的来源和生命周期
进一步追溯官方脚本及本地代码,补全了“相同身份”应检查的范围。下面均为源码事实,不是已通过收藏对照的修复方案。
| 字段/边界 | 官方或调用方语义 | 本地实际处理 |
|---|---|---|
WNMCID | 官方 injector 的普通网页路径在缺失时写入:6 个小写字母、首次生成时间、.01.0;保留已有值 | 缺失时为 12 个 hex 字符、当前请求时间、.01.0,不保存供下次调用使用 |
_ntes_nnid/_ntes_nuid | 官方 core 的 tracker 生成并保留 32 hex 访客标记;两个字段共享该标记,前者另带时间 | 缺失时用同次 32 bytes 随机数,即 64 hex;每次请求重新生成,分别补值也可能形成不关联的一对 |
deviceId | 调用方 Cookie 值优先,其次冻结到本次 Call 的运行时 state | 已登录普通请求不读取顶层 config.deviceId;仅固定该选项不能固定出站值 |
| 登录身份指纹 | 用于 SDK 内部区分登录身份 | 已登录时只哈希 MUSIC_U;不包含设备、平台、CSRF、IP 或临时 Cookie |
| Cookie 编码 | 调用方可能提供 Cookie 字符串 | 解析不做 URI 解码,发出时却做 URI 编码;已有 %xx 可能被再次编码为 %25xx |
| 响应 Cookie | 浏览器保存服务器下发值 | 普通 SDK 没有自动 Cookie jar,响应 Cookie 返回调用者,不自动更新 client |
本地依据为 request.ts、request-plan.ts、identity-snapshot.ts、utils.ts 和 transport.ts。官方依据为上述 core 的 tracker 段,以及 injector 的 Rt 生成函数和 Tr 缺失值处理。脚本设置的 Cookie 保存期限不能当作服务端有效期。
官方加密层本身只在业务字段之外补 CSRF,随机密钥生成没有读取当前时间;后续 NEJ transport 还可按页面配置给同源 URL 追加 CSRF query,现场确实带有该 query。没有显式时间字段不代表服务器不会按密文或操作历史判重。成功 SDK 探针也走过本地随机字段生成流程,因此格式不同不等于必然失败;不能顺手修改全局身份规则来代替本票因果验证。
已缩小的范围与仍缺少的证据
- “只有旧密文后来无法重放”不足以解释 17:50 的新官方请求失败;它仍可能解释部分旧重放结果,不能从本次结果反推 17:09 时官方新请求的状态。
- 请求内容、实际网络来源、时间和浏览器状态尚未同时受控。下一次现场验证必须先解决实际出口可观测、可固定的问题;本次
curl -6的单次读取成功没有做到这一点。不应把 IPv6 直接判为根因或建议用户禁用它。 - 该目标当天多次收藏、取消的历史是另一个候选因素。当前没有服务端阈值或同条件跨目标证据;不能把先前六次成功拟合成“每天六次上限”。恢复歌单列表也不代表清除了上游操作历史。
- 原有
os=iphone单变量反向对照仍成立,但不足以解释所有路径。待当前成功控制成立,才有条件逐项区分 Cookie 格式、持久化、协议与平台标记;现在没有依据实现第三份生产候选,也未启动跨日验收。
23:00 接续:新会话、官方 Mac 客户端抓包与两条拒绝规则
以上记录均来自本机、同一账号,时间均为北京时间。凭据、完整请求和抓包只保存在本机,不进入仓库。
新会话下的公开模块仍然被拒
- 约 22:30 发现原会话失效:
loginStatus的 eapi 和 weapi 两种写法都返回code: 200,但account、profile为 null。失效原因未查明(用户可能自行退出,也可能是上游风控),不作为因果证据。 - 23:06 用户重新登录并更新 Cookie;确认仍是同一账号。
- 23:08:32 公开模块(weapi、
os=web、浏览器 User-Agent)收藏他人歌单106697785。该目标从未被操作过,会话是刚登录的新会话。上游返回 HTTP 200/业务 405,前后读取完整列表摘要不变。按预登记规则立即停止:本组只发 1 次收藏写入、共 8 个请求,未执行旧写法对照。
这次结果同时排除了“旧会话被标记”“同一目标反复收藏”“iPhone 身份才被拒”三种解释,说明账号或路径级存在独立的频率拒绝。
官方 Mac 客户端收藏请求
经用户同意,在本机用 mitmproxy 抓取官方 Mac 客户端 3.1.13 的请求。证书、系统代理在抓包后恢复并删除;抓包期间没有修改客户端或上游内容,只读观察。抓取到 223 条网易云相关请求,无 TLS 证书失败。
23:22:44.323 客户端收藏同一张歌单(26467411),23:22:53.745 取消收藏,两次都是 HTTP 200/{ "code": 200 }。收藏请求与 SDK 当前写法的结构差异:
| 项目 | 官方 Mac 客户端(成功) | SDK 公开模块(405) |
|---|---|---|
| 地址 | https://interface.music.163.com/eapi/playlist/subscribe?_nmclfl=1 | https://music.163.com/weapi/playlist/subscribe |
| 身份 | Cookie os=osx;正文 os=OSX;appver=3.1.13;osver=27.0.1 | Cookie os=web;正文无 os |
| 设备 | clientSign(带 MAC 地址形态)、deviceId(正文 32 位、加密头 75 位、Cookie 73 位,三者不同)、brand=Apple、mode/mobilename=Mac15,6、buildver=3330 | 由 SDK 生成的进程级 deviceId,无 clientSign、brand、mode |
| 反作弊 | 收藏前 0.110 秒向 ac.dun.163.com/v3/b 申请新令牌;同一 170 字符值出现在正文 checkToken、加密头 X-antiCheatToken 和 HTTP 头 x-anticheattoken;另有 verifyId=1 | 均不发送,SDK 也无法正当地生成该令牌 |
| 取消收藏 | 同一身份,不带 checkToken 和反作弊头 | 不带 |
| 转发 IP | 无 X-Forwarded-For、X-Real-IP | 默认发送进程级伪装 IP |
| 响应 | 明文 {"code":200} | 业务 405 正文 |
客户端启动阶段还向易盾发起了 7 次 POST /v3/b 和 1 次 GET /v2/config/js,说明令牌由易盾 SDK 在本地现场生成,而不是服务端下发的固定值。这与 #32 的旧结论一致:写死的 checkToken 会被拒,上游用现场令牌的写法也被拒(那个实现把令牌当作可静态获取的配置,路径与官方客户端不同)。
结论与边界
- **收藏歌单受易盾反作弊保护。**官方客户端每次收藏都携带一个现场生成的一次性令牌;取消收藏不带。SDK 不内置令牌生成器,但可以接收调用方提供的令牌,并用官方现代 web player 的 weapi
os=web写法发送。 - **没有令牌的请求受更严的频率限制。**此前连续操作若干次后,SDK 与网页这条路会被冻结数小时(10-09 约 1.5 小时以上,10-17 从 17:02 持续到 23:08 仍未恢复),而同一账号的手机 App 一直正常,说明限制按路径或客户端桶计算,不是整号封禁。
- **自称 pc/iphone 客户端但缺少客户端特征会被直接拒绝。**10-09 的
os=pc、10-17 的os=iphone都与同会话的网页请求交错出现,只有前者被拒。官网网页身份可以消除这一类拒绝,但不能消除第 2 条。 - **令牌必须来自稳定设备/会话。**10-18 的接续实验用新鲜的 170 字符令牌让公开链路成功了一次;随后换成新的无头设备取令牌,连续 405。官方客户端能持续操作,是因为它每次收藏都从同一个已登记设备取新令牌。
因此修复范围是:未传 checkToken 时保留官网网页写法;传入 checkToken(字符串或 provider)时按官方现代 web player 的 weapi os=web 写法发送,正文携带 checkToken,HTTP 头携带 X-antiCheatToken,取消收藏走 weapi 且不带令牌。SDK 不生成、不缓存、不重放令牌,也不通过更换出口、目标或短间隔重试来绕过限制。两个相隔 24 小时窗口只能证明某一时刻的可用性,不能代替高频验证;高频验收应使用稳定设备上的 provider 连续执行。
10-18 接续:新鲜 token 单次成功与稳定设备约束
2026-10-18 01:07–01:20(CST)在本机真账号上做了单变量实验。所有写请求都先确认目标未收藏;失败后停止,未执行的轮次不计为通过。
| 时间 | 请求 | 结果 |
|---|---|---|
| 01:08:00 | 公开模块 weapi os=web,无 token | HTTP 200 / 业务 405;完整列表未变 |
| 01:15:08 | 公开链路 eapi os=pc + 本地 Watchman v2 现取的 170 字符 token(正文 checkToken + HTTP 头 X-antiCheatToken) | HTTP 200 / { code: 200 };无缓存列表 54 → 55;取消 200,恢复 54/36 |
| 01:15:33 | 同一写法,token 改从 v3 直取端点获取(32 字符) | 非 JSON 响应,SDK 报错;完整列表未变 |
| 01:18:49 | 公开模块 eapi os=pc + 新进程现取 token | HTTP 200 / 业务 405;完整列表未变 |
| 01:20:09 | 持久 Watchman 实例的第一枚 token(仍是新设备) | HTTP 200 / 业务 405;完整列表未变 |
| 01:20:52 | 官方 os=osx 设备字段 + 新 token | 响应无法解密;完整列表未变 |
| 01:47:15 | 真实客户端 origin https://campanula-test.caelum.moe/ 的官方 Watchman token + 公开模块(eapi 过渡验证) | 收藏 HTTP 200 / { code: 200 },列表 54 → 55;eapi os=pc 取消失败;weapi 回退 200,恢复 54/36 |
| 01:49:22–01:49:35 | 同一 campanula Watchman 实例连续 3 轮,轮间隔 5 秒;收藏 eapi + token,取消 weapi(过渡验证) | 3/3 通过;每轮列表 54 → 55 → 54,完整状态恢复 |
| 02:05 | 官方现代 web player 抓包 | /weapi/playlist/subscribe 200,X-antiCheatToken 170;/weapi/playlist/unsubscribe 200 无 token;JS 用 initWatchman + WM.getToken |
| 02:08:49 | campanula 真实 origin token + 公开 SDK weapi | 收藏 200,列表 54 → 55;取消 200,恢复 54/36 |
| 02:09:03–02:09:17 | 同一 campanula Watchman 实例连续 3 轮,轮间隔 5 秒;收藏 weapi + token,取消 weapi | 3/3 通过;每轮列表 54 → 55 → 54,完整状态恢复 |
可以确认:
- **官方现代 web player 的写法本身可用。**02:05 抓包确认:
/weapi/playlist/subscribe带 170 字符X-antiCheatToken返回 200;/weapi/playlist/unsubscribe不带 token 返回 200。页面 JS 在启动时initWatchman({ productNumber: 'YD00000558929251' }),每次收藏前调用window.WM.getToken,正文放checkToken,HTTP 头放X-antiCheatToken。 - **32 字符的 v3 直取 token 不等价。**它没有取得业务 200,不能替代 Watchman v2 的 170 字符令牌。
- **新无头设备会被拒绝。**首次成功后,新进程/新设备的令牌连续 405;因此不能靠每次新建浏览器实例来取 token。持久实例的第一枚 token 仍是新设备,也被拒,尚未取得第二枚 token 的对照。
- **官方 web player 的优势是稳定浏览器设备。**收藏前从同一个 Watchman 实例取新令牌;取消收藏不带令牌。稳定设备上下文是高频操作的必要条件,而不是可选的优化。
- **真实客户端 origin 可以生成被接受的 token。**02:08 在 campanula 测试页的浏览器 origin 里用官方 Watchman 取 token,公开 SDK 的 weapi 收藏成功(54 → 55),取消恢复 54/36;02:09 用同一个 Watchman 实例连续 3 轮、5 秒间隔,收藏/取消全部通过,每轮完整列表恢复。因此最终路径是 weapi + token:收藏带 token,取消不带。
因此 SDK 的修复是把令牌接入请求链路:checkToken 可以是字符串或每次订阅前调用的 provider;收藏(t=1)使用 weapi os=web,正文携带 checkToken,HTTP 头携带 X-antiCheatToken;取消(t=0)走 weapi 网页写法,不取令牌。令牌的获取与设备稳定性由调用方负责,SDK 不生成、不缓存、不重放。没有稳定 provider 时,网页回退路径只能用于低频调用;这不满足“高频收藏”的目标,不能把一次成功当作稳定性证明。