specific-ai-cli 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +177 -0
  3. package/README.ja.md +177 -0
  4. package/README.ko.md +177 -0
  5. package/README.md +174 -2
  6. package/README.zh-TW.md +177 -0
  7. package/assets/banner.txt +3 -0
  8. package/dist/cli/argv.js +98 -0
  9. package/dist/cli/commands/auth.js +11 -0
  10. package/dist/cli/commands/config.js +23 -0
  11. package/dist/cli/commands/doctor.js +134 -0
  12. package/dist/cli/commands/platforms.js +24 -0
  13. package/dist/cli/commands/version.js +4 -0
  14. package/dist/cli/confirm.js +22 -0
  15. package/dist/cli/dispatch.js +440 -0
  16. package/dist/cli/help.js +223 -0
  17. package/dist/cli/hints.js +60 -0
  18. package/dist/cli/main.js +18 -0
  19. package/dist/cli/output.js +66 -0
  20. package/dist/core/auth-handlers.js +41 -0
  21. package/dist/core/auth-store.js +119 -0
  22. package/dist/core/capabilities.js +163 -0
  23. package/dist/core/config.js +121 -0
  24. package/dist/core/cookies.js +164 -0
  25. package/dist/core/errors.js +37 -0
  26. package/dist/core/files.js +89 -0
  27. package/dist/core/fsutil.js +52 -0
  28. package/dist/core/hash.js +4 -0
  29. package/dist/core/http.js +331 -0
  30. package/dist/core/image.js +280 -0
  31. package/dist/core/log.js +40 -0
  32. package/dist/core/login.js +191 -0
  33. package/dist/core/mp4.js +130 -0
  34. package/dist/core/normalize.js +207 -0
  35. package/dist/core/options.js +110 -0
  36. package/dist/core/paths.js +38 -0
  37. package/dist/core/pb.js +59 -0
  38. package/dist/core/py.js +181 -0
  39. package/dist/core/rand.js +131 -0
  40. package/dist/core/registry.js +129 -0
  41. package/dist/core/schemas.js +41 -0
  42. package/dist/core/stream.js +104 -0
  43. package/dist/core/toolkit.js +35 -0
  44. package/dist/core/vm.js +73 -0
  45. package/dist/core/vocab.js +157 -0
  46. package/dist/platforms/_shared/impaas.js +410 -0
  47. package/dist/platforms/bilibili/index.js +216 -0
  48. package/dist/platforms/bilibili/web/api.js +806 -0
  49. package/dist/platforms/bilibili/web/client.js +311 -0
  50. package/dist/platforms/bilibili/web/commands.js +783 -0
  51. package/dist/platforms/bilibili/web/ddddocr.js +342 -0
  52. package/dist/platforms/bilibili/web/gaia.js +130 -0
  53. package/dist/platforms/bilibili/web/geetest-manual.js +154 -0
  54. package/dist/platforms/bilibili/web/geetest-vision.js +232 -0
  55. package/dist/platforms/bilibili/web/geetest.js +450 -0
  56. package/dist/platforms/bilibili/web/normalize.js +234 -0
  57. package/dist/platforms/bilibili/web/profile.js +135 -0
  58. package/dist/platforms/bilibili/web/resolve.js +112 -0
  59. package/dist/platforms/bilibili/web/sign.js +223 -0
  60. package/dist/platforms/bilibili/web/upos.js +102 -0
  61. package/dist/platforms/douyin/index.js +142 -0
  62. package/dist/platforms/douyin/web/api.js +863 -0
  63. package/dist/platforms/douyin/web/client.js +413 -0
  64. package/dist/platforms/douyin/web/commands.js +712 -0
  65. package/dist/platforms/douyin/web/creator.js +601 -0
  66. package/dist/platforms/douyin/web/crypto.js +247 -0
  67. package/dist/platforms/douyin/web/dtrait.js +190 -0
  68. package/dist/platforms/douyin/web/im.js +502 -0
  69. package/dist/platforms/douyin/web/image.js +5 -0
  70. package/dist/platforms/douyin/web/live.js +225 -0
  71. package/dist/platforms/douyin/web/mssdk.js +148 -0
  72. package/dist/platforms/douyin/web/normalize.js +326 -0
  73. package/dist/platforms/douyin/web/passport.js +1247 -0
  74. package/dist/platforms/douyin/web/profile.js +221 -0
  75. package/dist/platforms/douyin/web/proto.js +26 -0
  76. package/dist/platforms/douyin/web/resolve.js +88 -0
  77. package/dist/platforms/douyin/web/sign.js +436 -0
  78. package/dist/platforms/douyin/web/tos.js +78 -0
  79. package/dist/platforms/index.js +17 -0
  80. package/dist/platforms/jd/index.js +67 -0
  81. package/dist/platforms/jd/web/aks.js +115 -0
  82. package/dist/platforms/jd/web/api.js +549 -0
  83. package/dist/platforms/jd/web/chat.js +132 -0
  84. package/dist/platforms/jd/web/client.js +470 -0
  85. package/dist/platforms/jd/web/commands.js +557 -0
  86. package/dist/platforms/jd/web/jcap/helper.js +90 -0
  87. package/dist/platforms/jd/web/jcap/host.js +107 -0
  88. package/dist/platforms/jd/web/jcap/lsd.js +300 -0
  89. package/dist/platforms/jd/web/jcap/solver.js +995 -0
  90. package/dist/platforms/jd/web/jcap.js +150 -0
  91. package/dist/platforms/jd/web/js.js +420 -0
  92. package/dist/platforms/jd/web/login.js +775 -0
  93. package/dist/platforms/jd/web/normalize.js +297 -0
  94. package/dist/platforms/jd/web/profile.js +256 -0
  95. package/dist/platforms/jd/web/util.js +175 -0
  96. package/dist/platforms/jd/web/webm.js +133 -0
  97. package/dist/platforms/kuaishou/index.js +98 -0
  98. package/dist/platforms/kuaishou/web/api.js +694 -0
  99. package/dist/platforms/kuaishou/web/captcha-crypto.js +134 -0
  100. package/dist/platforms/kuaishou/web/captcha-fp.js +180 -0
  101. package/dist/platforms/kuaishou/web/captcha.js +273 -0
  102. package/dist/platforms/kuaishou/web/client.js +487 -0
  103. package/dist/platforms/kuaishou/web/commands.js +863 -0
  104. package/dist/platforms/kuaishou/web/gap.js +144 -0
  105. package/dist/platforms/kuaishou/web/gdfp.js +376 -0
  106. package/dist/platforms/kuaishou/web/gql.js +11 -0
  107. package/dist/platforms/kuaishou/web/live.js +125 -0
  108. package/dist/platforms/kuaishou/web/normalize.js +241 -0
  109. package/dist/platforms/kuaishou/web/oracle.js +329 -0
  110. package/dist/platforms/kuaishou/web/profile.js +259 -0
  111. package/dist/platforms/kuaishou/web/resolve.js +84 -0
  112. package/dist/platforms/kuaishou/web/session.js +592 -0
  113. package/dist/platforms/kuaishou/web/sign.js +568 -0
  114. package/dist/platforms/taobao/index.js +56 -0
  115. package/dist/platforms/taobao/web/api.js +96 -0
  116. package/dist/platforms/taobao/web/client.js +102 -0
  117. package/dist/platforms/taobao/web/commands.js +172 -0
  118. package/dist/platforms/taobao/web/im.js +68 -0
  119. package/dist/platforms/taobao/web/normalize.js +103 -0
  120. package/dist/platforms/taobao/web/profile.js +85 -0
  121. package/dist/platforms/taobao/web/resolve.js +51 -0
  122. package/dist/platforms/taobao/web/sign.js +35 -0
  123. package/dist/platforms/tiktok/index.js +120 -0
  124. package/dist/platforms/tiktok/web/api.js +1234 -0
  125. package/dist/platforms/tiktok/web/client.js +362 -0
  126. package/dist/platforms/tiktok/web/commands.js +821 -0
  127. package/dist/platforms/tiktok/web/im.js +133 -0
  128. package/dist/platforms/tiktok/web/jsrun.js +126 -0
  129. package/dist/platforms/tiktok/web/normalize.js +286 -0
  130. package/dist/platforms/tiktok/web/profile.js +169 -0
  131. package/dist/platforms/tiktok/web/resolve.js +156 -0
  132. package/dist/platforms/tiktok/web/sign.js +585 -0
  133. package/dist/platforms/tiktok/web/upload.js +805 -0
  134. package/dist/platforms/tiktok/web/wire.js +490 -0
  135. package/dist/platforms/weibo/index.js +91 -0
  136. package/dist/platforms/weibo/web/api.js +206 -0
  137. package/dist/platforms/weibo/web/client.js +155 -0
  138. package/dist/platforms/weibo/web/commands.js +202 -0
  139. package/dist/platforms/weibo/web/normalize.js +137 -0
  140. package/dist/platforms/weibo/web/profile.js +246 -0
  141. package/dist/platforms/weibo/web/resolve.js +60 -0
  142. package/dist/platforms/weibo/web/sign.js +85 -0
  143. package/dist/platforms/x/index.js +107 -0
  144. package/dist/platforms/x/web/api.js +443 -0
  145. package/dist/platforms/x/web/article.js +212 -0
  146. package/dist/platforms/x/web/client.js +202 -0
  147. package/dist/platforms/x/web/commands.js +363 -0
  148. package/dist/platforms/x/web/graphql.js +108 -0
  149. package/dist/platforms/x/web/normalize.js +235 -0
  150. package/dist/platforms/x/web/profile.js +99 -0
  151. package/dist/platforms/x/web/resolve.js +44 -0
  152. package/dist/platforms/x/web/transaction.js +171 -0
  153. package/dist/platforms/xhs/index.js +101 -0
  154. package/dist/platforms/xhs/web/api.js +278 -0
  155. package/dist/platforms/xhs/web/client.js +540 -0
  156. package/dist/platforms/xhs/web/commands.js +1046 -0
  157. package/dist/platforms/xhs/web/creator-api.js +609 -0
  158. package/dist/platforms/xhs/web/creator.js +203 -0
  159. package/dist/platforms/xhs/web/js.js +194 -0
  160. package/dist/platforms/xhs/web/login.js +230 -0
  161. package/dist/platforms/xhs/web/normalize.js +166 -0
  162. package/dist/platforms/xhs/web/profile.js +127 -0
  163. package/dist/platforms/xhs/web/push.js +361 -0
  164. package/dist/platforms/xhs/web/resolve.js +69 -0
  165. package/dist/platforms/xhs/web/state.js +368 -0
  166. package/dist/platforms/xianyu/index.js +59 -0
  167. package/dist/platforms/xianyu/web/api.js +312 -0
  168. package/dist/platforms/xianyu/web/client.js +134 -0
  169. package/dist/platforms/xianyu/web/commands.js +305 -0
  170. package/dist/platforms/xianyu/web/im.js +73 -0
  171. package/dist/platforms/xianyu/web/normalize.js +123 -0
  172. package/dist/platforms/xianyu/web/profile.js +146 -0
  173. package/dist/platforms/xianyu/web/resolve.js +48 -0
  174. package/dist/platforms/xianyu/web/sign.js +79 -0
  175. package/package.json +77 -4
  176. package/static/douyin/Live.proto +194 -0
  177. package/static/douyin/Request.proto +89 -0
  178. package/static/douyin/Response.proto +46 -0
  179. package/static/douyin/acrawler_runtime/ac_vm.js +2 -0
  180. package/static/douyin/acrawler_runtime/browser_window_shape.json +1 -0
  181. package/static/douyin/acrawler_runtime/canvas_actual_exact.json +1 -0
  182. package/static/douyin/acrawler_runtime/run_ac_node.js +1005 -0
  183. package/static/douyin/acrawler_runtime/runner_window_keys_baseline.txt +240 -0
  184. package/static/douyin/challenge_profile.json +801 -0
  185. package/static/douyin/challenge_template_runner.js +217 -0
  186. package/static/douyin/dtrait_profile.json +66 -0
  187. package/static/douyin/mstoken_common_profile.json +1 -0
  188. package/static/douyin/mstoken_profile.json +214 -0
  189. package/static/jd/h5st5_env.js +750 -0
  190. package/static/jd/h5st5_lib.js +5 -0
  191. package/static/jd/h5st5_server.js +49 -0
  192. package/static/jd/jcap/env/env_core.js +512 -0
  193. package/static/jd/jcap/env/run.js +1307 -0
  194. package/static/jd/jcap/run/jcap_ujb96b.js +39 -0
  195. package/static/jd/package.json +4 -0
  196. package/static/jd/pc_tk_lib.js +1 -0
  197. package/static/jd/pc_tk_server.js +63 -0
  198. package/static/jd/summer_cryptico_runner.js +50 -0
  199. package/static/jd/webm/env/run.js +453 -0
  200. package/static/jd/webm/run/jdwebm-riskhandle.js +49 -0
  201. package/static/kuaishou/bundles/weapon/kwf-0.0.2.2cee19b4b7dec496.js +1 -0
  202. package/static/kuaishou/bundles/weapon/kws-0-0.0.1-obfuscated.4b7f3de1ab0ddcc2.js +1 -0
  203. package/static/kuaishou/bundles/weapon/kws-1-0.0.1-obfuscated.346ebdb1c78df316.js +1 -0
  204. package/static/kuaishou/bundles/weapon/kws-10-0.0.1-obfuscated.a33db41897b983af.js +1 -0
  205. package/static/kuaishou/bundles/weapon/kws-11-0.0.1-obfuscated.5e0a90af726d8a7e.js +1 -0
  206. package/static/kuaishou/bundles/weapon/kws-12-0.0.1-obfuscated.20ea29345706ec8c.js +1 -0
  207. package/static/kuaishou/bundles/weapon/kws-13-0.0.1-obfuscated.6b74e9640ff18648.js +1 -0
  208. package/static/kuaishou/bundles/weapon/kws-14-0.0.1-obfuscated.551e4429d156bf66.js +1 -0
  209. package/static/kuaishou/bundles/weapon/kws-15-0.0.1-obfuscated.8625b087aaba4a60.js +1 -0
  210. package/static/kuaishou/bundles/weapon/kws-16-0.0.1-obfuscated.d5ffa4020f3be99a.js +1 -0
  211. package/static/kuaishou/bundles/weapon/kws-17-0.0.1-obfuscated.6d13e19b22f50a80.js +1 -0
  212. package/static/kuaishou/bundles/weapon/kws-18-0.0.1-obfuscated.6d31405044166289.js +1 -0
  213. package/static/kuaishou/bundles/weapon/kws-19-0.0.1-obfuscated.a9a8d925942f19e4.js +1 -0
  214. package/static/kuaishou/bundles/weapon/kws-2-0.0.1-obfuscated.b849a136ae4a6b3e.js +1 -0
  215. package/static/kuaishou/bundles/weapon/kws-3-0.0.1-obfuscated.f9e77ebf6186e2ac.js +1 -0
  216. package/static/kuaishou/bundles/weapon/kws-4-0.0.1-obfuscated.2c3c155aa52ceb0e.js +1 -0
  217. package/static/kuaishou/bundles/weapon/kws-5-0.0.1-obfuscated.4e553d17691b3c5a.js +1 -0
  218. package/static/kuaishou/bundles/weapon/kws-6-0.0.1-obfuscated.f73ddd3ff13e1217.js +1 -0
  219. package/static/kuaishou/bundles/weapon/kws-7-0.0.1-obfuscated.1628b8729beaab7e.js +1 -0
  220. package/static/kuaishou/bundles/weapon/kws-8-0.0.1-obfuscated.6da6baef7aa81fff.js +1 -0
  221. package/static/kuaishou/bundles/weapon/kws-9-0.0.1-obfuscated.8645d55e5323b743.js +1 -0
  222. package/static/kuaishou/fixtures/current_follow_script_813.network-response +2 -0
  223. package/static/kuaishou/fixtures/live_ws_maps.json +61 -0
  224. package/static/kuaishou/fixtures/live_ws_proto.json +1342 -0
  225. package/static/kuaishou/js/cp-kwf.js +1 -0
  226. package/static/kuaishou/tools/kwf_oracle.js +799 -0
  227. package/static/kuaishou/tools/kws_oracle.js +885 -0
  228. package/static/kuaishou/tools/like_token_oracle.js +70 -0
  229. package/static/taobao/taobao_js_20260407.js +570 -0
  230. package/static/tiktok/Tiktok_Request.json +597 -0
  231. package/static/tiktok/package.json +3 -0
  232. package/static/tiktok/shop_bsid/core.js +1 -0
  233. package/static/tiktok/shop_bsid/env_core.js +512 -0
  234. package/static/tiktok/shop_bsid/loader.js +1 -0
  235. package/static/tiktok/shop_bsid/run.js +753 -0
  236. package/static/tiktok/shop_bsid/sign.js +100 -0
  237. package/static/tiktok/shop_bsid/webmssdk.js +1 -0
  238. package/static/tiktok/shop_bsid/webmssdk_ex.js +1 -0
  239. package/static/tiktok/signing/env/env_core.js +512 -0
  240. package/static/tiktok/signing/env/sign.js +455 -0
  241. package/static/tiktok/signing/sdk_config.json +230 -0
  242. package/static/tiktok/signing/source/webmssdk.js +1 -0
  243. package/static/tiktok/signing/source/webmssdk_ex.js +1 -0
  244. package/static/tiktok/signing/source/webmssdk_legacy.js +1 -0
  245. package/static/x/graphql.json +7777 -0
  246. package/static/x/transaction_l1.json +12 -0
  247. package/static/xhs/package.json +3 -0
  248. package/static/xhs/xhs_core/js/b1.js +210 -0
  249. package/static/xhs/xhs_core/js/mns.js +340 -0
  250. package/static/xhs/xhs_core/js/mns_0101_keystream.json +5 -0
  251. package/static/xhs/xhs_core/js/mns_keystreams.json +1 -0
  252. package/static/xhs/xhs_core/js/sign.js +225 -0
  253. package/static/xhs/xhs_core/js/websectiga_cli.js +220 -0
  254. package/static/xhs/xhs_core/js/websectiga_env.js +512 -0
  255. package/static/xhs/xhs_core/js/xs_common.js +79 -0
  256. package/static/xhs/xhs_creator/js/profile.js +115 -0
  257. package/static/xhs/xhs_creator/js/rap_fingerprint_creator.json +6 -0
  258. package/static/xhs/xhs_creator/js/reference_profile.json +282 -0
  259. package/static/xhs/xhs_creator/js/xhs_creator_sign.js +29 -0
  260. package/static/xhs/xhs_creator/js/xhs_creator_signature.js +23 -0
  261. package/static/xhs/xhs_pc/js/aes_decrypt.js +22 -0
  262. package/static/xhs/xhs_pc/js/aes_encrypt.js +30 -0
  263. package/static/xhs/xhs_pc/js/b1.js +9 -0
  264. package/static/xhs/xhs_pc/js/deflate.js +320 -0
  265. package/static/xhs/xhs_pc/js/mns.js +4 -0
  266. package/static/xhs/xhs_pc/js/profile.js +141 -0
  267. package/static/xhs/xhs_pc/js/rap.js +173 -0
  268. package/static/xhs/xhs_pc/js/rap_cli.js +30 -0
  269. package/static/xhs/xhs_pc/js/rap_crypto.js +73 -0
  270. package/static/xhs/xhs_pc/js/rap_fingerprint_template.json +8 -0
  271. package/static/xhs/xhs_pc/js/reference_profile.json +262 -0
  272. package/static/xhs/xhs_pc/js/sign.js +6 -0
  273. package/static/xhs/xhs_pc/js/web_ssk.js +82 -0
  274. package/static/xhs/xhs_pc/js/xs_common.js +4 -0
  275. package/static/xianyu/et_f.js +2 -0
  276. package/static/xianyu/gen_tfstk.js +410 -0
  277. package/static/xianyu/goofish_js_version_2.js +568 -0
@@ -0,0 +1,362 @@
1
+ import { SpecificAIError } from '../../../core/errors.js';
2
+ import { jsonLoads, unquote } from '../../../core/py.js';
3
+ import * as rand from '../../../core/rand.js';
4
+ import { authError, httpClient, isGuest } from '../../../core/toolkit.js';
5
+ import { BROWSER, buildHeaders, COOKIE_DOMAIN, ORIGIN, Params, PROFILE, WEBCAST } from './profile.js';
6
+ import { SignerError, signRequest, ticketGuardHeaders } from './sign.js';
7
+ const CONTENT_TYPE_BEFORE_MOBILE = [
8
+ 'sec-ch-ua-platform', 'referer', 'content-encoding', 'user-agent', 'sec-ch-ua', 'content-type', 'sec-ch-ua-mobile', 'accept',
9
+ 'accept-encoding', 'accept-language', 'content-length', 'cookie', 'origin', 'priority', 'sec-fetch-dest', 'sec-fetch-mode', 'sec-fetch-site',
10
+ ];
11
+ function ticketGuardOrder(ttCsrf) {
12
+ return [
13
+ 'tt-ticket-guard-client-data', 'sec-ch-ua-platform', 'referer', 'sec-ch-ua', 'sec-ch-ua-mobile', 'tt-ticket-guard-web-version',
14
+ ttCsrf ? 'tt-csrf-token' : '__no_tt_csrf__', 'tt-ticket-guard-version', 'user-agent', 'tt-ticket-guard-public-key', 'x-secsdk-csrf-token',
15
+ 'content-type', 'tt-ticket-guard-iteration-version', 'accept', 'accept-encoding', 'accept-language', 'content-length', 'cookie',
16
+ 'origin', 'priority', 'sec-fetch-dest', 'sec-fetch-mode', 'sec-fetch-site',
17
+ ];
18
+ }
19
+ const SV_CHARSET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
20
+ const MSTOKEN_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
21
+ export function byteLength(body) {
22
+ return typeof body === 'string' ? Buffer.byteLength(body, 'utf8') : body.length;
23
+ }
24
+ export class TikTok {
25
+ ctx;
26
+ http;
27
+ jar;
28
+ device;
29
+ browser;
30
+ /** device_id / odin_id 来自浏览器证据(Tea cache、multi_sids、会话 JSON 或首页引导),不是本地随机的。 */
31
+ deviceExplicit = false;
32
+ odinExplicit = false;
33
+ deviceAmbiguous = false;
34
+ constructor(ctx) {
35
+ this.ctx = ctx;
36
+ this.http = httpClient(ctx, { browser: BROWSER, os: 'windows' });
37
+ this.jar = this.http.jar;
38
+ this.device = ctx.credential.device;
39
+ this.browser = {
40
+ ua: this.device.user_agent || PROFILE.ua,
41
+ secChUa: this.device.sec_ch_ua || PROFILE.secChUa,
42
+ secChUaPlatform: this.device.sec_ch_ua_platform || PROFILE.secChUaPlatform,
43
+ acceptLanguage: this.device.accept_language || PROFILE.acceptLanguage,
44
+ };
45
+ this.syncIds();
46
+ }
47
+ // ---------------------------------------------------------------- 浏览器状态
48
+ get ua() {
49
+ return this.browser.ua;
50
+ }
51
+ get clientAbVersions() {
52
+ return this.device.client_ab_versions || PROFILE.clientAbVersions;
53
+ }
54
+ get deviceId() {
55
+ return this.device.device_id ?? '';
56
+ }
57
+ get odinId() {
58
+ return this.device.odin_id || this.deviceId;
59
+ }
60
+ get region() {
61
+ return this.device.region || 'SG';
62
+ }
63
+ get priorityRegion() {
64
+ return this.device.priority_region ?? 'SG';
65
+ }
66
+ get metrics() {
67
+ return this.device.browser_metrics ?? {};
68
+ }
69
+ cookie(name) {
70
+ return this.jar.get(name) ?? '';
71
+ }
72
+ /** 发给所有 tiktok.com 子域的 cookie 串(上游 cookie_str),签名 JS 也读它。 */
73
+ get cookieStr() {
74
+ return this.jar.header(ORIGIN + '/');
75
+ }
76
+ /** WebIdLastTime:会话里给出的值,其次是浏览器存储的 g_exp,最后是首次使用的时间(之后固定)。 */
77
+ get webIdLastTime() {
78
+ if (this.device.web_id_last_time)
79
+ return String(this.device.web_id_last_time);
80
+ const gExp = this.device.local_storage?.g_exp ?? this.device.session_storage?.g_exp;
81
+ if (gExp != null && /^\d+$/.test(String(gExp))) {
82
+ const v = Number(gExp);
83
+ return String(v > 1e11 ? Math.floor(v / 1000) : v);
84
+ }
85
+ return (this.device.web_id_last_time = String(rand.nowSeconds()));
86
+ }
87
+ /** 查询签名用的 msToken:sessionStorage > localStorage > cookie。 */
88
+ get msToken() {
89
+ return this.device.session_storage?.msToken || this.device.local_storage?.msToken || this.cookie('msToken');
90
+ }
91
+ get ttwid() {
92
+ return unquote(String(this.metrics.im_ttwid || this.cookie('ttwid') || ''));
93
+ }
94
+ get verifyFp() {
95
+ return this.cookie('s_v_web_id');
96
+ }
97
+ get loggedIn() {
98
+ return Boolean(this.cookie('sessionid') || this.cookie('sid_tt') || this.cookie('multi_sids'));
99
+ }
100
+ /** 当前登录用户的 uid(multi_sids 的第一段)。 */
101
+ get uid() {
102
+ const multi = this.cookie('multi_sids');
103
+ return multi ? multi.split('%3A')[0].split(':')[0] : '';
104
+ }
105
+ get ttCsrfToken() {
106
+ return this.device.tt_csrf_token || this.cookie('tt_csrf_token');
107
+ }
108
+ get secsdkCsrfToken() {
109
+ return this.device.secsdk_csrf_token ?? '';
110
+ }
111
+ /** 上游 _sync_ids_from_cookies:device_id 取会话 > 唯一的 Tea cache > multi_sids。 */
112
+ syncIds() {
113
+ const d = this.device;
114
+ if (d.device_id)
115
+ this.deviceExplicit = true;
116
+ if (d.odin_id)
117
+ this.odinExplicit = true;
118
+ if (!d.device_id) {
119
+ const candidates = [];
120
+ for (const storage of [d.local_storage ?? {}, d.session_storage ?? {}]) {
121
+ for (const [key, value] of Object.entries(storage)) {
122
+ if (!key.startsWith('__tea_cache_tokens_'))
123
+ continue;
124
+ try {
125
+ const id = String(jsonLoads(String(value)).web_id ?? '');
126
+ if (/^\d{16,20}$/.test(id) && !candidates.includes(id))
127
+ candidates.push(id);
128
+ }
129
+ catch { }
130
+ }
131
+ }
132
+ if (candidates.length === 1) {
133
+ d.device_id = candidates[0];
134
+ this.deviceExplicit = true;
135
+ }
136
+ else if (candidates.length > 1)
137
+ this.deviceAmbiguous = true;
138
+ }
139
+ const uid = this.uid;
140
+ if (!d.device_id && uid) {
141
+ d.device_id = uid;
142
+ this.deviceExplicit = true;
143
+ }
144
+ if (!d.odin_id && uid) {
145
+ d.odin_id = uid;
146
+ this.odinExplicit = true;
147
+ }
148
+ }
149
+ /**
150
+ * 签名请求需要的浏览器状态(上游 require_browser_profile)。上游缺什么就报错;specific-ai 按下面的方式补齐并保存:
151
+ * - device_id / odin_id / WebIdLastTime:请求一次首页,取 hydration 里的 wid / odinId / webIdCreatedTime;
152
+ * - s_v_web_id(verifyFp):按浏览器格式本地生成;
153
+ * - msToken:本地生成(浏览器里由 mssdk 下发,服务端对只读接口不校验来源)。
154
+ */
155
+ async prepare() {
156
+ if (this.deviceAmbiguous) {
157
+ throw new SpecificAIError('USAGE', '会话里的 Tea cache 有多个 device_id', { hint: '在会话 JSON 里显式给出 device_id 后重新登录' });
158
+ }
159
+ if (!this.deviceExplicit || !this.odinExplicit)
160
+ await this.bootstrap();
161
+ if (!this.verifyFp)
162
+ this.jar.set('s_v_web_id', svWebId(), COOKIE_DOMAIN);
163
+ if (!this.msToken)
164
+ this.jar.set('msToken', rand.string(148, MSTOKEN_CHARSET), COOKIE_DOMAIN);
165
+ }
166
+ /** 首页 hydration 的 webapp.app-context:服务端分配的 wid(device_id)与 odinId。 */
167
+ async bootstrap() {
168
+ const html = await this.document(ORIGIN + '/', ORIGIN + '/');
169
+ const context = hydration(html)?.['webapp.app-context'] ?? {};
170
+ const d = this.device;
171
+ if (!this.deviceExplicit && context.wid) {
172
+ d.device_id = String(context.wid);
173
+ this.deviceExplicit = true;
174
+ }
175
+ if (!this.odinExplicit && context.odinId) {
176
+ d.odin_id = String(context.odinId);
177
+ this.odinExplicit = true;
178
+ }
179
+ if (!d.web_id_last_time && context.webIdCreatedTime)
180
+ d.web_id_last_time = String(context.webIdCreatedTime);
181
+ if (!this.deviceExplicit)
182
+ throw new SpecificAIError('UPSTREAM', 'TikTok 首页没有返回设备 ID(wid)', { detail: { keys: Object.keys(context) } });
183
+ if (!this.odinExplicit) {
184
+ d.odin_id = d.device_id;
185
+ this.odinExplicit = true;
186
+ }
187
+ }
188
+ /** tt-ticket-guard:需要浏览器 security-sdk 的私钥与票据,只能从会话 JSON 导入。 */
189
+ ticketGuard(path) {
190
+ const d = this.device;
191
+ const state = {
192
+ privateKey: d.ticket_guard_private_key ?? '',
193
+ encryptTicket: d.ticket_guard_encrypt_ticket ?? '',
194
+ tsSign: d.ticket_guard_ts_sign ?? '',
195
+ version: d.ticket_guard_version,
196
+ iterationVersion: d.ticket_guard_iteration_version,
197
+ };
198
+ const missing = Object.entries({ ticket_guard_private_key: state.privateKey, ticket_guard_encrypt_ticket: state.encryptTicket, ticket_guard_ts_sign: state.tsSign })
199
+ .filter(([, v]) => !v)
200
+ .map(([k]) => k);
201
+ if (missing.length)
202
+ throw missingSession(this.ctx, `这个操作需要浏览器 security-sdk 的 ticket-guard 数据(缺少 ${missing.join('、')})`);
203
+ return ticketGuardHeaders(state, path);
204
+ }
205
+ requireLogin() {
206
+ if (!this.loggedIn)
207
+ throw authError(this.ctx, isGuest(this.ctx) ? undefined : '当前账号缺少 sessionid / sid_tt / multi_sids,请重新登录');
208
+ }
209
+ // ---------------------------------------------------------------- 请求
210
+ headers(type, o) {
211
+ return buildHeaders(type, this.browser, o);
212
+ }
213
+ /** 发送;带 Set-Cookie 的响应由 cookie 罐合并回凭证。 */
214
+ send(req) {
215
+ return this.http.request(req);
216
+ }
217
+ /** 上游 _request_response:组请求头 → 可选 ticket-guard → 签名 → 发送。 */
218
+ async requestResponse(o) {
219
+ const method = o.method.toUpperCase();
220
+ const isWebcast = o.path.startsWith('/webcast/') || o.path.startsWith('/tiktok/event/');
221
+ const requestOrigin = isWebcast ? WEBCAST : ORIGIN;
222
+ const body = o.body ?? null;
223
+ const type = method === 'POST' && body != null ? (o.form ? 'FORM' : 'POST') : 'GET';
224
+ const h = this.headers(type, {
225
+ referer: o.referer,
226
+ origin: o.origin || (isWebcast ? ORIGIN : ''),
227
+ contentLength: body != null && ['POST', 'PUT', 'PATCH'].includes(method) ? String(byteLength(body)) : null,
228
+ secFetchSite: isWebcast ? 'same-site' : 'same-origin',
229
+ });
230
+ if (o.accept != null)
231
+ h.set('accept', o.accept);
232
+ if (o.extraHeaders)
233
+ h.update(o.extraHeaders);
234
+ if (o.headerOrder)
235
+ h.reorder(o.headerOrder);
236
+ if (o.contentTypeBeforeMobile && !o.ticketGuard)
237
+ h.reorder(CONTENT_TYPE_BEFORE_MOBILE, true);
238
+ if (o.ticketGuard) {
239
+ const secCsrf = o.ticketGuardSecCsrf ?? true;
240
+ const ttCsrf = o.ticketGuardTtCsrfHeader ?? true;
241
+ const guard = this.ticketGuard(o.path);
242
+ if (ttCsrf && !this.ttCsrfToken)
243
+ throw missingSession(this.ctx, '这个写操作需要 tt-csrf-token(cookie tt_csrf_token)');
244
+ if (secCsrf && !this.secsdkCsrfToken)
245
+ throw missingSession(this.ctx, '这个写操作需要 x-secsdk-csrf-token(会话 JSON 的 secsdk_csrf_token)');
246
+ h.update(guard);
247
+ if (ttCsrf)
248
+ h.set('tt-csrf-token', this.ttCsrfToken);
249
+ if (secCsrf)
250
+ h.set('x-secsdk-csrf-token', this.secsdkCsrfToken);
251
+ h.reorder(ticketGuardOrder(ttCsrf), true);
252
+ }
253
+ // 在副本上签名:重试时 o.params 仍是未签名的
254
+ const params = o.params.clone();
255
+ if (o.signed ?? true) {
256
+ let unsigned = params.toQuery();
257
+ if (!unsigned.includes('msToken='))
258
+ unsigned += `&msToken=${this.msToken}`;
259
+ const url = `${requestOrigin}${o.path}?${unsigned}`;
260
+ try {
261
+ params.update(signRequest({ url, method, body, userAgent: this.ua, referer: o.referer, metrics: this.metrics }));
262
+ }
263
+ catch (err) {
264
+ if (err instanceof SignerError)
265
+ throw new SpecificAIError('ERROR', `${o.path} 纯计算签名失败:${err.message}`);
266
+ throw err;
267
+ }
268
+ }
269
+ return this.send({ method, url: `${requestOrigin}${o.path}?${params.toQuery()}`, headers: h.pairs(), body: body ?? undefined });
270
+ }
271
+ /** 上游 _request_json:HTTP 错误、空响应、业务错误码映射成 specific-ai 的错误。 */
272
+ async requestJson(o) {
273
+ const res = await this.requestResponse(o);
274
+ try {
275
+ return await this.json(res, { allowEmpty: o.allowEmpty, path: o.path });
276
+ }
277
+ catch (err) {
278
+ // 新设备的头几次请求偶尔拿到空响应;只读请求重签一次
279
+ if (!(err instanceof SpecificAIError && err.code === 'RISK_CONTROL' && err.detail?.kind === 'blocked' && o.method.toUpperCase() === 'GET'))
280
+ throw err;
281
+ this.ctx.log.debug(`${o.path} 返回空响应,重试一次`);
282
+ await rand.sleep(1500);
283
+ return this.json(await this.requestResponse(o), { allowEmpty: o.allowEmpty, path: o.path });
284
+ }
285
+ }
286
+ async json(res, o = {}) {
287
+ const text = await res.text();
288
+ if (res.status === 429)
289
+ throw new SpecificAIError('RISK_CONTROL', 'TikTok 限流(HTTP 429),请稍后再试', { detail: { kind: 'rate_limit', status: 429 } });
290
+ if (!text) {
291
+ if (res.status >= 400)
292
+ throw new SpecificAIError('UPSTREAM', `TikTok 返回 HTTP ${res.status}`, { detail: { status: res.status, path: o.path } });
293
+ if (o.allowEmpty)
294
+ return { http_status: res.status, status_code: null, empty_response: true };
295
+ throw new SpecificAIError('RISK_CONTROL', 'TikTok 返回了空响应(签名或设备环境被拒绝)', { detail: { kind: 'blocked', path: o.path } });
296
+ }
297
+ let body;
298
+ try {
299
+ body = jsonLoads(text);
300
+ }
301
+ catch {
302
+ throw new SpecificAIError('UPSTREAM', `TikTok 返回的不是 JSON(HTTP ${res.status})`, { detail: { status: res.status, path: o.path, body: text.slice(0, 300) } });
303
+ }
304
+ check(this.ctx, body);
305
+ if (res.status >= 400)
306
+ throw new SpecificAIError('UPSTREAM', `TikTok 返回 HTTP ${res.status}`, { detail: { status: res.status, path: o.path, body: text.slice(0, 300) } });
307
+ return body;
308
+ }
309
+ /** 页面 HTML(上游 get_user_html)。 */
310
+ async document(url, referer = url) {
311
+ const res = await this.send({ url, headers: this.headers('DOC', { referer }).pairs() });
312
+ if (res.status >= 400)
313
+ throw new SpecificAIError('UPSTREAM', `TikTok 页面返回 HTTP ${res.status}`, { detail: { status: res.status, url } });
314
+ return res.text();
315
+ }
316
+ }
317
+ /** 业务状态码:status_code / statusCode 非 0 时报错。 */
318
+ export function check(ctx, body) {
319
+ if (!body || typeof body !== 'object' || Array.isArray(body))
320
+ return;
321
+ const code = body.status_code ?? body.statusCode;
322
+ // privacy config 这类接口用 statusCode: 200 表示成功
323
+ if (code == null || code === 0 || code === '0' || code === 200)
324
+ return;
325
+ const message = String(body.status_msg ?? body.statusMsg ?? body.message ?? '');
326
+ const c = Number(code);
327
+ // 8:Login expired;20003:webcast 的 User doesn't login
328
+ if (c === 8 || c === 20003)
329
+ throw authError(ctx, isGuest(ctx) ? undefined : message || undefined);
330
+ if (c === 10000) {
331
+ throw new SpecificAIError('RISK_CONTROL', `TikTok 风控拦截:${message || code}`, { detail: { kind: 'captcha', code, message } });
332
+ }
333
+ throw new SpecificAIError('UPSTREAM', message || `TikTok 返回错误 ${code}`, { detail: { code, message } });
334
+ }
335
+ function missingSession(ctx, message) {
336
+ const account = isGuest(ctx) ? '' : ` -a ${ctx.account}`;
337
+ return new SpecificAIError('AUTH_REQUIRED', message, { hint: `用浏览器会话 JSON 重新登录:specific-ai tiktok auth login${account} --cookie @tiktok-session.json` });
338
+ }
339
+ /** s_v_web_id:`verify_<base36 毫秒>_<8>_<4>_4<3>_[89ab]<3>_<12>`。 */
340
+ export function svWebId() {
341
+ const ts = rand.now().toString(36);
342
+ const r = (n) => rand.string(n, SV_CHARSET);
343
+ return `verify_${ts}_${r(8)}_${r(4)}_4${r(3)}_${rand.choice('89ab')}${r(3)}_${r(12)}`;
344
+ }
345
+ /** 页面里的 `__UNIVERSAL_DATA_FOR_REHYDRATION__`,返回 `__DEFAULT_SCOPE__`。 */
346
+ export function hydration(html) {
347
+ const m = /<script[^>]*id="__UNIVERSAL_DATA_FOR_REHYDRATION__"[^>]*>([\s\S]*?)<\/script>/.exec(html);
348
+ if (!m)
349
+ return null;
350
+ try {
351
+ return jsonLoads(m[1]).__DEFAULT_SCOPE__ ?? null;
352
+ }
353
+ catch {
354
+ return null;
355
+ }
356
+ }
357
+ /** 建立会话,补齐签名请求需要的设备数据(见 {@link TikTok.prepare})。 */
358
+ export async function tiktok(ctx) {
359
+ const t = new TikTok(ctx);
360
+ await t.prepare();
361
+ return t;
362
+ }