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,1234 @@
1
+ import { SpecificAIError } from '../../../core/errors.js';
2
+ import { compactJson, jsonLoads, quote } from '../../../core/py.js';
3
+ import * as rand from '../../../core/rand.js';
4
+ import { hydration } from './client.js';
5
+ import { ORIGIN, Params, SHOP } from './profile.js';
6
+ import { encodeXBogus } from './sign.js';
7
+ import { shopSign } from './jsrun.js';
8
+ const bversion = (t) => t.ua.split('Mozilla/').slice(1).join('Mozilla/') || t.ua;
9
+ /** 上游 _current_params:5.3.x 的 query,端点字段放在固定的槽位。 */
10
+ export function currentParams(t, o) {
11
+ const slots = o.slots ?? {};
12
+ const queryRef = o.queryReferer ?? o.referer;
13
+ const root = o.rootReferer ?? queryRef;
14
+ const values = [];
15
+ const add = (k, v) => {
16
+ if (v !== undefined && v !== null)
17
+ values.push([k, v]);
18
+ };
19
+ const slot = (name) => (slots[name] ?? []).forEach(([k, v]) => add(k, v));
20
+ add('WebIdLastTime', t.webIdLastTime);
21
+ slot('before_aid');
22
+ add('aid', o.aid ?? '1988');
23
+ slot('after_aid');
24
+ slot('before_app_language');
25
+ add('app_language', o.appLanguage ?? 'zh-Hans');
26
+ add('app_name', 'tiktok_web');
27
+ slot('after_app_name');
28
+ add('browser_language', 'zh-CN');
29
+ add('browser_name', 'Mozilla');
30
+ add('browser_online', 'true');
31
+ slot('after_browser_online');
32
+ add('browser_platform', 'Win32');
33
+ add('browser_version', bversion(t));
34
+ slot('after_browser_version');
35
+ add('channel', 'tiktok_web');
36
+ slot('after_channel');
37
+ add('cookie_enabled', 'true');
38
+ slot('after_cookie');
39
+ add('data_collection_enabled', 'true');
40
+ slot('after_data_collection');
41
+ add('device_id', t.deviceId);
42
+ add('device_platform', o.devicePlatform ?? 'web_pc');
43
+ slot('after_device_platform');
44
+ add('focus_state', 'true');
45
+ slot('after_focus');
46
+ if (o.includeFromPage ?? true)
47
+ add('from_page', o.fromPage ?? 'user');
48
+ slot('after_from_page');
49
+ add('history_len', o.historyLen ?? '2');
50
+ slot('after_history');
51
+ add('is_fullscreen', 'false');
52
+ slot('after_fullscreen');
53
+ add('is_page_visible', 'true');
54
+ slot('after_is_page_visible');
55
+ if (o.includeOdinId ?? true)
56
+ add('odinId', t.odinId);
57
+ add('os', 'windows');
58
+ slot('after_os');
59
+ add('priority_region', t.priorityRegion);
60
+ slot('after_priority_region');
61
+ add('referer', queryRef);
62
+ add('region', t.region);
63
+ slot('after_region');
64
+ add('root_referer', root);
65
+ slot('after_root_referer');
66
+ add('screen_height', '1440');
67
+ add('screen_width', '2560');
68
+ slot('after_screen');
69
+ add('tz_name', 'Asia/Shanghai');
70
+ slot('after_tz');
71
+ if (o.includeUserIsLogin ?? true)
72
+ add('user_is_login', t.loggedIn ? 'true' : 'false');
73
+ add('verifyFp', t.verifyFp);
74
+ slot('after_verify_fp');
75
+ add('webcast_language', 'zh-Hans');
76
+ slot('after_webcast_language');
77
+ return new Params(values);
78
+ }
79
+ /** 上游 Params.common:较早一代端点的 query 顺序。OrderedDict 的重复键保留第一次出现的位置。 */
80
+ export function commonParams(t, o) {
81
+ const queryRef = o.queryReferer ?? o.referer;
82
+ const root = o.rootReferer ?? queryRef;
83
+ const values = new Map();
84
+ const put = (k, v) => values.set(k, v == null ? v : String(v));
85
+ const emit = (s) => (s ?? []).forEach(([k, v]) => put(k, v));
86
+ put('WebIdLastTime', t.webIdLastTime);
87
+ put('aid', '1988');
88
+ emit(o.afterAid);
89
+ put('app_language', 'zh-Hans');
90
+ put('app_name', 'tiktok_web');
91
+ emit(o.afterAppName);
92
+ put('browser_language', 'zh-CN');
93
+ put('browser_name', 'Mozilla');
94
+ put('browser_online', 'true');
95
+ put('browser_platform', 'Win32');
96
+ put('browser_version', bversion(t));
97
+ put('channel', 'tiktok_web');
98
+ put('cookie_enabled', 'true');
99
+ emit(o.afterCookie);
100
+ emit(o.early);
101
+ put('data_collection_enabled', 'true');
102
+ put('device_id', t.deviceId);
103
+ put('device_platform', 'web_pc');
104
+ emit(o.afterDevicePlatform);
105
+ put('focus_state', 'true');
106
+ if (o.includeFromPage)
107
+ put('from_page', o.fromPage ?? '');
108
+ put('history_len', o.historyLen ?? '2');
109
+ put('is_fullscreen', 'false');
110
+ put('is_page_visible', 'true');
111
+ emit(o.afterIsPageVisible);
112
+ put('language', 'zh-Hans');
113
+ put('odinId', t.odinId);
114
+ put('os', 'windows');
115
+ emit(o.afterOs);
116
+ put('priority_region', t.priorityRegion);
117
+ put('referer', queryRef);
118
+ put('region', t.region);
119
+ emit(o.afterRegion);
120
+ if (o.includeRootReferer ?? true)
121
+ put('root_referer', root);
122
+ emit(o.afterRootReferer);
123
+ put('screen_height', '1440');
124
+ put('screen_width', '2560');
125
+ emit(o.beforeTz);
126
+ put('tz_name', 'Asia/Shanghai');
127
+ if (o.includeUserIsLogin)
128
+ put('user_is_login', t.loggedIn ? 'true' : 'false');
129
+ put('verifyFp', t.verifyFp);
130
+ emit(o.late);
131
+ put('webcast_language', 'zh-Hans');
132
+ if (o.includeLanguage === false)
133
+ values.delete('language');
134
+ return new Params(values);
135
+ }
136
+ const AXIOS_ACCEPT = 'application/json, text/plain, */*';
137
+ const PROFILE_LIST_ORDER = [
138
+ 'x-cthulhu-csrf', 'sec-ch-ua-platform', 'referer', 'sec-ch-ua', 'sec-ch-ua-mobile', 'tt-csrf-token', 'user-agent', 'content-type', 'accept',
139
+ 'accept-encoding', 'accept-language', 'content-length', 'cookie', 'origin', 'priority', 'sec-fetch-dest', 'sec-fetch-mode', 'sec-fetch-site',
140
+ ];
141
+ const profileRef = (secUid) => `${ORIGIN}/@${secUid}?lang=zh-Hans`;
142
+ const DEFAULT_REF = `${ORIGIN}/@tiktok?lang=zh-Hans`;
143
+ // ================================================================ 用户与作品
144
+ export function userPosted(t, secUid, cursor = '0', count = '24', referer) {
145
+ const ref = referer ?? profileRef(secUid);
146
+ const p = commonParams(t, {
147
+ referer: ref,
148
+ rootReferer: ref,
149
+ historyLen: '2',
150
+ includeFromPage: false,
151
+ includeUserIsLogin: true,
152
+ early: [
153
+ ['count', count],
154
+ ['coverFormat', '2'],
155
+ ['cursor', cursor],
156
+ ],
157
+ beforeTz: [['secUid', secUid]],
158
+ late: [['video_encoding', 'dash']],
159
+ });
160
+ return t.requestJson({ method: 'GET', path: '/api/post/item_list/', params: p, referer: ref });
161
+ }
162
+ export function recommendFeed(t, o = {}) {
163
+ const ref = o.referer ?? `${ORIGIN}/`;
164
+ const p = currentParams(t, {
165
+ referer: ref,
166
+ queryReferer: '',
167
+ rootReferer: '',
168
+ fromPage: 'fyp',
169
+ historyLen: '2',
170
+ includeUserIsLogin: false,
171
+ slots: {
172
+ after_channel: [['clientABVersions', t.clientAbVersions]],
173
+ after_cookie: [
174
+ ['count', o.count ?? '6'],
175
+ ['coverFormat', '2'],
176
+ ['cpu_core_number', '20'],
177
+ ['dark_mode', 'false'],
178
+ ],
179
+ after_data_collection: [['day_of_week', o.dayOfWeek ?? '0']],
180
+ after_device_platform: [['enable_cache', 'false']],
181
+ after_history: [['isNonPersonalized', 'false']],
182
+ after_fullscreen: [['is_new_user', 'true']],
183
+ after_is_page_visible: [['language', 'zh-Hans']],
184
+ after_screen: [
185
+ ['showAboutThisAd', 'true'],
186
+ ['showAds', 'true'],
187
+ ['time_of_day', o.timeOfDay ?? '5'],
188
+ ],
189
+ after_verify_fp: [
190
+ ['video_encoding', 'dash'],
191
+ ['vv_count', '0'],
192
+ ['vv_count_fyp', '0'],
193
+ ['watchLiveLastTime', o.watchLiveLastTime ?? String(rand.now())],
194
+ ],
195
+ },
196
+ });
197
+ // pullType 在 priority_region 与 referer 之间
198
+ const items = [];
199
+ for (const pair of p.pairs) {
200
+ items.push(pair);
201
+ if (pair[0] === 'priority_region')
202
+ items.push(['pullType', '1']);
203
+ }
204
+ return t.requestJson({ method: 'GET', path: '/api/recommend/item_list/', params: new Params(items), referer: ref });
205
+ }
206
+ export function userPlaylist(t, secUid, cursor = '0', count = '20') {
207
+ const ref = profileRef(secUid);
208
+ const p = commonParams(t, {
209
+ referer: ref,
210
+ rootReferer: ref,
211
+ historyLen: '2',
212
+ includeUserIsLogin: true,
213
+ includeLanguage: false,
214
+ early: [
215
+ ['count', count],
216
+ ['cursor', cursor],
217
+ ],
218
+ beforeTz: [['secUid', secUid]],
219
+ });
220
+ return t.requestJson({ method: 'GET', path: '/api/user/playlist/', params: p, referer: ref });
221
+ }
222
+ /** /api/user/list/:scene 67 为粉丝,21 为关注。 */
223
+ export function profileUserList(t, secUid, scene, o = {}) {
224
+ const ref = profileRef(secUid);
225
+ const p = currentParams(t, {
226
+ referer: ref,
227
+ queryReferer: ref,
228
+ rootReferer: ref,
229
+ fromPage: 'user',
230
+ historyLen: '2',
231
+ slots: {
232
+ after_channel: [['clientABVersions', t.clientAbVersions]],
233
+ after_cookie: [['count', o.count ?? '30']],
234
+ after_is_page_visible: [
235
+ ['maxCursor', o.maxCursor ?? '0'],
236
+ ['minCursor', o.minCursor ?? '0'],
237
+ ],
238
+ after_root_referer: [['scene', scene]],
239
+ after_screen: [['secUid', secUid]],
240
+ },
241
+ });
242
+ return t.requestJson({ method: 'GET', path: '/api/user/list/', params: p, referer: ref });
243
+ }
244
+ export function followingItemList(t, cursor = '0', count = '9') {
245
+ const ref = `${ORIGIN}/following?lang=zh-Hans`;
246
+ const p = currentParams(t, {
247
+ referer: ref,
248
+ queryReferer: '',
249
+ rootReferer: '',
250
+ fromPage: 'following',
251
+ historyLen: '2',
252
+ slots: {
253
+ after_channel: [['clientABVersions', t.clientAbVersions]],
254
+ after_cookie: [
255
+ ['count', count],
256
+ ['coverFormat', '2'],
257
+ ['cursor', cursor],
258
+ ],
259
+ after_history: [
260
+ ['isNonPersonalized', 'false'],
261
+ ['isResetCounterUsed', 'true'],
262
+ ],
263
+ after_is_page_visible: [
264
+ ['language', 'zh-Hans'],
265
+ ['level', '1'],
266
+ ],
267
+ after_priority_region: [['pullType', '1']],
268
+ },
269
+ });
270
+ return t.requestJson({
271
+ method: 'GET',
272
+ path: '/api/following/item_list/',
273
+ params: p,
274
+ referer: ref,
275
+ ticketGuard: true,
276
+ ticketGuardSecCsrf: false,
277
+ ticketGuardTtCsrfHeader: false,
278
+ });
279
+ }
280
+ // ================================================================ 通知
281
+ export function noticeCount(t, referer = `${ORIGIN}/`, fromPage = 'fyp', historyLen = '2') {
282
+ const p = currentParams(t, { referer, queryReferer: '', rootReferer: '', fromPage, historyLen });
283
+ return t.requestJson({ method: 'GET', path: '/api/inbox/notice_count/', params: p, referer });
284
+ }
285
+ /**
286
+ * 收件箱另一组通知(上游 get_inbox_notice_list):默认 group 661,即网页收件箱的「系统通知」(All=661,下分 TikTok、
287
+ * 账号更新、LIVE、创作者变现等来源)。私信页的变体(上游 get_message_notice_list)只是 referer / from_page / history_len 不同。
288
+ */
289
+ export function inboxNoticeList(t, groupList, o = {}) {
290
+ const referer = o.referer ?? `${ORIGIN}/`;
291
+ const groups = groupList ?? [{ count: 1, is_mark_read: 0, group: 661, max_time: 0, min_time: 0 }];
292
+ const p = currentParams(t, {
293
+ referer,
294
+ queryReferer: '',
295
+ rootReferer: '',
296
+ fromPage: o.fromPage ?? 'fyp',
297
+ historyLen: o.historyLen ?? '2',
298
+ slots: { after_from_page: [['group_list', compactJson(groups)]] },
299
+ });
300
+ return t.requestJson({ method: 'GET', path: '/api/inbox/notice_list/', params: p, referer });
301
+ }
302
+ /** 私信页:私信页上下文的请求(收件箱通知、用户卡片、IM 的 im_headers)用它作 referer。 */
303
+ export const MESSAGES_PAGE = `${ORIGIN}/messages?lang=zh-Hans`;
304
+ /** 私信页上下文的收件箱通知(上游 get_message_notice_list)。specific-ai 的命令没有用到(notice list 走首页上下文),保留供对拍。 */
305
+ export const messageNoticeList = (t, groupList) => inboxNoticeList(t, groupList, { referer: MESSAGES_PAGE, fromPage: 'message', historyLen: '4' });
306
+ export function noticeMulti(t, groupList, referer) {
307
+ const ref = referer ?? DEFAULT_REF;
308
+ const groups = groupList ?? [{ count: 20, is_mark_read: 0, group: 500, max_time: 0, min_time: 0 }];
309
+ const p = currentParams(t, {
310
+ referer: ref,
311
+ queryReferer: '',
312
+ rootReferer: '',
313
+ fromPage: 'fyp',
314
+ historyLen: '2',
315
+ slots: { after_from_page: [['group_list', compactJson(groups)]] },
316
+ });
317
+ return t.requestJson({ method: 'GET', path: '/api/notice/multi/', params: p, referer: ref });
318
+ }
319
+ /** 私信页的用户卡片(不签名):uid → unique_id 等。 */
320
+ export function imUserProfile(t, userIds) {
321
+ const ref = MESSAGES_PAGE;
322
+ const p = new Params([
323
+ ['aid', '1988'],
324
+ ['user_ids', JSON.stringify(userIds.map(String))],
325
+ ]);
326
+ return t.requestJson({ method: 'GET', path: '/tiktok/v1/im/user/profile/', params: p, referer: ref, signed: false });
327
+ }
328
+ // ================================================================ 收藏夹、转发、收藏
329
+ /** 个人主页的收藏夹 / 转发 / 收藏 tab 的 query(上游 _profile_item_list_params)。 */
330
+ function profileItemListParams(t, o) {
331
+ const pairs = [
332
+ ['WebIdLastTime', t.webIdLastTime],
333
+ ['aid', '1988'],
334
+ ];
335
+ if (o.includeAppId)
336
+ pairs.push(['appId', '1988']);
337
+ pairs.push(['app_language', 'zh-Hans'], ['app_name', 'tiktok_web'], ['browser_language', 'zh-CN'], ['browser_name', 'Mozilla'], ['browser_online', 'true'], ['browser_platform', 'Win32'], ['browser_version', bversion(t)], ['channel', 'tiktok_web']);
338
+ if (o.includeClientAb)
339
+ pairs.push(['clientABVersions', t.clientAbVersions]);
340
+ pairs.push(['cookie_enabled', 'true'], ['count', o.count], ['coverFormat', '2'], ['cursor', o.cursor], ['data_collection_enabled', 'true'], ['device_id', t.deviceId], ['device_platform', 'web_pc'], ['focus_state', 'true'], ['from_page', 'user'], ['history_len', '6'], ['is_fullscreen', 'false'], ['is_page_visible', 'true'], ['language', 'zh-Hans'], ['needPinnedItemIds', 'true'], ['odinId', t.odinId], ['os', 'windows'], ['post_item_list_request_type', '0'], ['priority_region', t.priorityRegion]);
341
+ if (o.includePublicOnly)
342
+ pairs.push(['publicOnly', 'true']);
343
+ pairs.push(['referer', o.rootReferer], ['region', t.region], ['root_referer', ''], ['screen_height', '1440'], ['screen_width', '2560'], ['secUid', o.secUid], ['tz_name', 'Asia/Shanghai'], ['user_is_login', t.loggedIn ? 'true' : 'false'], ['verifyFp', t.verifyFp], ['video_encoding', 'dash'], ['webcast_language', 'zh-Hans']);
344
+ return new Params(pairs);
345
+ }
346
+ export function collectionList(t, secUid, cursor = '0', count = '30') {
347
+ const ref = profileRef(secUid);
348
+ const p = profileItemListParams(t, { secUid, cursor, count, rootReferer: ref, includeAppId: true, includePublicOnly: true });
349
+ return t.requestJson({ method: 'GET', path: '/api/user/collection_list/', params: p, referer: ref });
350
+ }
351
+ export function repostList(t, secUid, cursor = '0', count = '30') {
352
+ const ref = profileRef(secUid);
353
+ const p = profileItemListParams(t, { secUid, cursor, count, rootReferer: ref, includeClientAb: true });
354
+ return t.requestJson({ method: 'GET', path: '/api/repost/item_list/', params: p, referer: ref });
355
+ }
356
+ export function collectedItemList(t, secUid, cursor = '0', count = '16') {
357
+ const ref = profileRef(secUid);
358
+ const p = profileItemListParams(t, { secUid, cursor, count, rootReferer: ref, includeClientAb: true });
359
+ return t.requestJson({ method: 'GET', path: '/api/user/collect/item_list/', params: p, referer: ref });
360
+ }
361
+ export function checkPlaylistName(t, name, referer = DEFAULT_REF, historyLen = '7') {
362
+ const p = currentParams(t, { referer, queryReferer: referer, rootReferer: '', fromPage: 'user', historyLen, slots: { after_is_page_visible: [['name', name]] } });
363
+ return t.requestJson({ method: 'GET', path: '/api/playlist/name_check', params: p, referer });
364
+ }
365
+ function collectionWriteHeaders(t) {
366
+ if (!t.ttCsrfToken)
367
+ throw new SpecificAIError('AUTH_REQUIRED', '收藏夹写入缺少 tt-csrf-token(cookie tt_csrf_token)', { hint: 'specific-ai tiktok auth login --cookie @tiktok-session.json' });
368
+ return { 'x-cthulhu-csrf': '1', 'tt-csrf-token': t.ttCsrfToken };
369
+ }
370
+ export function collectionCreate(t, name, status = '1', referer = DEFAULT_REF) {
371
+ if (!name)
372
+ throw new SpecificAIError('USAGE', '收藏夹名字不能为空');
373
+ if ([...name].length > 30)
374
+ throw new SpecificAIError('USAGE', '收藏夹名字不能超过 30 个字符');
375
+ const headers = collectionWriteHeaders(t);
376
+ const p = currentParams(t, {
377
+ referer,
378
+ queryReferer: referer,
379
+ rootReferer: '',
380
+ fromPage: 'user',
381
+ historyLen: '6',
382
+ slots: {
383
+ after_channel: [
384
+ ['collectionName', name],
385
+ ['collectionStatus', status],
386
+ ],
387
+ after_is_page_visible: [['language', 'zh-Hans']],
388
+ },
389
+ });
390
+ return t.requestJson({ method: 'POST', path: '/api/collection/create/', params: p, referer, origin: ORIGIN, body: '', form: true, extraHeaders: headers, headerOrder: PROFILE_LIST_ORDER });
391
+ }
392
+ export function collectionModifyInfo(t, collectionId, name, status = '1', referer) {
393
+ if (!collectionId || !name)
394
+ throw new SpecificAIError('USAGE', '修改收藏夹需要收藏夹 ID 和新名字');
395
+ if ([...name].length > 30)
396
+ throw new SpecificAIError('USAGE', '收藏夹名字不能超过 30 个字符');
397
+ const headers = collectionWriteHeaders(t);
398
+ const ref = referer ?? `${ORIGIN}/@tiktok/collection/${collectionId}`;
399
+ const p = currentParams(t, {
400
+ referer: ref,
401
+ queryReferer: ref,
402
+ rootReferer: '',
403
+ fromPage: 'user',
404
+ historyLen: '7',
405
+ slots: {
406
+ after_channel: [
407
+ ['collectionId', collectionId],
408
+ ['collectionName', name],
409
+ ['collectionStatus', status],
410
+ ],
411
+ after_is_page_visible: [['language', 'zh-Hans']],
412
+ },
413
+ });
414
+ return t.requestJson({ method: 'POST', path: '/api/collection/modify_info/', params: p, referer: ref, origin: ORIGIN, body: '', form: true, extraHeaders: headers, headerOrder: PROFILE_LIST_ORDER });
415
+ }
416
+ /** 收藏夹的 collectionStatus:网页 IDL 的 CollectionStatus 枚举,1 为私密(上游默认值),3 为公开。 */
417
+ export const COLLECTION_STATUS = { private: '1', public: '3' };
418
+ /**
419
+ * 把视频加入收藏夹(上游 post_collection_modify_items):业务字段全在签名 query 里,form body 为空。
420
+ * commitIds 只接受一个 ID(上游只有单个视频的浏览器样本,不猜多个 ID 的分隔符)。
421
+ * referer 是触发请求的视频页,profileUrl 是 query 里 referer / root_referer 用的主页 URL。
422
+ */
423
+ export function collectionModifyItems(t, collectionId, commitIds, o) {
424
+ for (const [name, value] of [
425
+ ['collectionId', collectionId],
426
+ ['commitIds', commitIds],
427
+ ]) {
428
+ if (!value || value !== value.trim())
429
+ throw new SpecificAIError('USAGE', `收藏夹写入需要非空的 ${name}`);
430
+ }
431
+ for (const [name, value] of [
432
+ ['referer', o.referer],
433
+ ['profile_url', o.profileUrl],
434
+ ]) {
435
+ const u = new URL(value);
436
+ if (u.protocol !== 'https:' || u.host !== 'www.tiktok.com')
437
+ throw new SpecificAIError('ERROR', `collection/modify_items 的 ${name} 必须是完整的 https://www.tiktok.com 页面 URL`);
438
+ }
439
+ if (!new URL(o.referer).pathname.includes('/video/'))
440
+ throw new SpecificAIError('ERROR', 'collection/modify_items 的 referer 必须是视频详情页');
441
+ if (!new URL(o.profileUrl).pathname.startsWith('/@'))
442
+ throw new SpecificAIError('ERROR', 'collection/modify_items 的 profile_url 必须是主页 URL');
443
+ const headers = collectionWriteHeaders(t);
444
+ const p = new Params([
445
+ ['WebIdLastTime', t.webIdLastTime],
446
+ ['aid', '1988'],
447
+ ['app_language', 'zh-Hans'],
448
+ ['app_name', 'tiktok_web'],
449
+ ['browser_language', 'zh-CN'],
450
+ ['browser_name', 'Mozilla'],
451
+ ['browser_online', 'true'],
452
+ ['browser_platform', 'Win32'],
453
+ ['browser_version', bversion(t)],
454
+ ['channel', 'tiktok_web'],
455
+ ['collectionId', collectionId],
456
+ ['commitIds', commitIds],
457
+ ['cookie_enabled', 'true'],
458
+ ['data_collection_enabled', 'true'],
459
+ ['device_id', t.deviceId],
460
+ ['device_platform', 'web_pc'],
461
+ ['focus_state', 'true'],
462
+ ['from_page', 'video'],
463
+ ['history_len', '3'],
464
+ ['is_fullscreen', 'false'],
465
+ ['is_page_visible', 'true'],
466
+ ['language', 'zh-Hans'],
467
+ ['odinId', t.odinId],
468
+ ['os', 'windows'],
469
+ ['priority_region', t.priorityRegion],
470
+ ['referer', o.profileUrl],
471
+ ['region', t.region],
472
+ ['root_referer', o.profileUrl],
473
+ ['screen_height', '1440'],
474
+ ['screen_width', '2560'],
475
+ ['tz_name', 'Asia/Shanghai'],
476
+ ['user_is_login', t.loggedIn ? 'true' : 'false'],
477
+ ['verifyFp', t.verifyFp],
478
+ ['webcast_language', 'zh-Hans'],
479
+ ]);
480
+ return t.requestJson({ method: 'POST', path: '/api/collection/modify_items/', params: p, referer: o.referer, origin: ORIGIN, body: '', form: true, extraHeaders: headers, headerOrder: PROFILE_LIST_ORDER });
481
+ }
482
+ /** 收藏夹详情 / 内容的 query(上游 _collection_folder_params)。 */
483
+ function collectionFolderParams(t, o) {
484
+ const pairs = [
485
+ ['WebIdLastTime', t.webIdLastTime],
486
+ ['aid', '1988'],
487
+ ['app_language', 'zh-Hans'],
488
+ ['app_name', 'tiktok_web'],
489
+ ['browser_language', 'zh-CN'],
490
+ ['browser_name', 'Mozilla'],
491
+ ['browser_online', 'true'],
492
+ ['browser_platform', 'Win32'],
493
+ ['browser_version', bversion(t)],
494
+ ['channel', 'tiktok_web'],
495
+ ['clientABVersions', t.clientAbVersions],
496
+ ['collectionId', o.collectionId],
497
+ ['cookie_enabled', 'true'],
498
+ ];
499
+ if (o.sourceType != null)
500
+ pairs.push(['count', o.count ?? '30'], ['cursor', o.cursor ?? '0']);
501
+ pairs.push(['data_collection_enabled', 'true'], ['device_id', t.deviceId], ['device_platform', 'web_pc'], ['focus_state', 'true'], ['from_page', 'user'], ['history_len', o.historyLen], ['is_fullscreen', 'false'], ['is_page_visible', 'true'], ['language', 'zh-Hans'], ['odinId', t.odinId], ['os', 'windows'], ['priority_region', t.priorityRegion], ['referer', o.referer], ['region', t.region], ['root_referer', '']);
502
+ if (o.scene != null)
503
+ pairs.push(['scene', o.scene]);
504
+ pairs.push(['screen_height', '1440'], ['screen_width', '2560']);
505
+ if (o.sourceType != null)
506
+ pairs.push(['sourceType', o.sourceType]);
507
+ pairs.push(['tz_name', 'Asia/Shanghai'], ['user_is_login', t.loggedIn ? 'true' : 'false'], ['verifyFp', t.verifyFp], ['webcast_language', 'zh-Hans']);
508
+ return new Params(pairs);
509
+ }
510
+ export function collectionDetail(t, collectionId, scene = '116') {
511
+ const ref = `${ORIGIN}/@tiktok/collection/${collectionId}`;
512
+ const p = collectionFolderParams(t, { collectionId, historyLen: '7', scene, referer: ref });
513
+ return t.requestJson({ method: 'GET', path: '/api/collection/detail/', params: p, referer: ref });
514
+ }
515
+ export function collectionItemList(t, collectionId, cursor = '0', count = '30', sourceType = '113') {
516
+ const ref = `${ORIGIN}/@tiktok/collection/${collectionId}`;
517
+ const p = collectionFolderParams(t, { collectionId, historyLen: '7', scene: null, referer: ref, cursor, count, sourceType });
518
+ return t.requestJson({ method: 'GET', path: '/api/collection/item_list/', params: p, referer: ref });
519
+ }
520
+ // ================================================================ 搜索
521
+ export function searchLiveRoom(t, keyword, referer = `${ORIGIN}/live`) {
522
+ const p = currentParams(t, {
523
+ referer,
524
+ queryReferer: '',
525
+ rootReferer: '',
526
+ fromPage: 'search',
527
+ historyLen: '2',
528
+ slots: { after_from_page: [['get_hot_event_rooms', 'v0']], after_priority_region: [['query', keyword]] },
529
+ });
530
+ return t.requestJson({ method: 'GET', path: '/webcast/room/search/', params: p, referer, origin: ORIGIN });
531
+ }
532
+ export function searchGeneral(t, keyword, o = {}) {
533
+ const ref = o.referer ?? `${ORIGIN}/search?q=${quote(keyword, '')}&lang=zh-Hans`;
534
+ const code = compactJson({
535
+ tiktok: { client_params_x: { search_engine: { ies_mt_user_live_video_card_use_libra: 1, mt_search_general_user_live_card: 1 } }, search_server: {} },
536
+ });
537
+ const p = currentParams(t, {
538
+ referer: ref,
539
+ queryReferer: '',
540
+ rootReferer: '',
541
+ fromPage: 'search',
542
+ historyLen: '2',
543
+ slots: {
544
+ after_channel: [['client_ab_versions', t.clientAbVersions]],
545
+ after_cookie: [
546
+ ['count', o.count ?? '12'],
547
+ ['cursor', o.cursor ?? '0'],
548
+ ],
549
+ after_device_platform: [['device_type', 'web_h265']],
550
+ after_fullscreen: [['is_non_personalized_search', '0']],
551
+ after_is_page_visible: [['keyword', keyword]],
552
+ after_os: [['offset', o.offset ?? '0']],
553
+ after_verify_fp: [['web_search_code', code]],
554
+ },
555
+ });
556
+ return t.requestJson({ method: 'GET', path: '/api/search/general/full/', params: p, referer: ref });
557
+ }
558
+ export function searchSuggest(t, keyword, reqSource = 'related_search') {
559
+ if (!keyword)
560
+ throw new SpecificAIError('USAGE', '搜索建议需要非空的关键词');
561
+ const ref = `${ORIGIN}/search?q=${quote(keyword, '')}`;
562
+ const p = currentParams(t, {
563
+ referer: ref,
564
+ queryReferer: '',
565
+ rootReferer: '',
566
+ fromPage: 'search',
567
+ historyLen: '2',
568
+ slots: { after_is_page_visible: [['keyword', keyword]], after_region: [['req_source', reqSource]] },
569
+ });
570
+ return t.requestJson({ method: 'GET', path: '/api/search/suggest/guide/', params: p, referer: ref, signed: false });
571
+ }
572
+ // ================================================================ 评论与互动
573
+ export function comments(t, awemeId, cursor = '0', count = '20', referer = DEFAULT_REF) {
574
+ const p = currentParams(t, {
575
+ referer,
576
+ fromPage: 'video',
577
+ historyLen: '3',
578
+ appLanguage: 'ja-JP',
579
+ slots: {
580
+ after_app_name: [['aweme_id', awemeId]],
581
+ after_cookie: [
582
+ ['count', count],
583
+ ['current_region', t.region],
584
+ ['cursor', cursor],
585
+ ],
586
+ after_device_platform: [['enter_from', 'tiktok_web']],
587
+ after_focus: [['fromWeb', '1']],
588
+ after_fullscreen: [['is_non_personalized', 'false']],
589
+ },
590
+ });
591
+ return t.requestJson({ method: 'GET', path: '/api/comment/list/', params: p, referer });
592
+ }
593
+ export function commentReplies(t, itemId, commentId, o) {
594
+ for (const [name, value] of [
595
+ ['referer', o.referer],
596
+ ['root_referer', o.rootReferer],
597
+ ]) {
598
+ const u = new URL(value);
599
+ if (u.protocol !== 'https:' || u.host !== 'www.tiktok.com' || !u.pathname.includes('/video/')) {
600
+ throw new SpecificAIError('ERROR', `comment/list/reply 的 ${name} 必须是完整 TikTok 视频页 URL`);
601
+ }
602
+ }
603
+ const p = currentParams(t, {
604
+ referer: o.referer,
605
+ queryReferer: o.referer,
606
+ rootReferer: o.rootReferer,
607
+ fromPage: 'video',
608
+ historyLen: '6',
609
+ slots: {
610
+ after_channel: [['comment_id', commentId]],
611
+ after_cookie: [
612
+ ['count', o.count ?? '3'],
613
+ ['cursor', o.cursor ?? '1'],
614
+ ],
615
+ after_is_page_visible: [['item_id', itemId]],
616
+ },
617
+ });
618
+ return t.requestJson({ method: 'GET', path: '/api/comment/list/reply/', params: p, referer: o.referer });
619
+ }
620
+ export function postComment(t, awemeId, text, o = {}) {
621
+ if (!text)
622
+ throw new SpecificAIError('USAGE', '评论内容不能为空');
623
+ const ref = o.referer ?? `${ORIGIN}/@tiktok/video/${awemeId}?lang=zh-Hans`;
624
+ const qref = o.queryReferer ?? DEFAULT_REF;
625
+ const slots = {
626
+ after_app_name: [['aweme_id', awemeId]],
627
+ after_screen: [
628
+ ['text', text],
629
+ ['text_extra', o.textExtra ?? '[]'],
630
+ ],
631
+ };
632
+ if (o.replyId) {
633
+ slots.after_region = [
634
+ ['reply_id', o.replyId],
635
+ ['reply_to_reply_id', o.replyToReplyId ?? '0'],
636
+ ];
637
+ }
638
+ const p = currentParams(t, { referer: ref, queryReferer: qref, rootReferer: qref, fromPage: 'video', historyLen: '3', slots });
639
+ return t.requestJson({ method: 'POST', path: '/api/comment/publish/', params: p, referer: ref, origin: ORIGIN, body: '', form: true, ticketGuard: true });
640
+ }
641
+ export function itemDigg(t, awemeId, diggType, o = {}) {
642
+ const ref = o.referer ?? `${ORIGIN}/@tiktok/video/${awemeId}?lang=zh-Hans`;
643
+ const qref = o.queryReferer ?? DEFAULT_REF;
644
+ const p = currentParams(t, {
645
+ referer: ref,
646
+ queryReferer: qref,
647
+ rootReferer: qref,
648
+ fromPage: 'video',
649
+ historyLen: '3',
650
+ slots: { after_app_name: [['aweme_id', awemeId]], after_screen: [['type', diggType]] },
651
+ });
652
+ return t.requestJson({ method: 'POST', path: '/api/commit/item/digg/', params: p, referer: ref, origin: ORIGIN, body: '', form: true, ticketGuard: true, ticketGuardSecCsrf: false });
653
+ }
654
+ export function itemCollect(t, itemId, secUid, action, o = {}) {
655
+ const ref = o.referer ?? `${ORIGIN}/@tiktok/video/${itemId}?lang=zh-Hans`;
656
+ const qref = o.queryReferer ?? DEFAULT_REF;
657
+ const p = currentParams(t, {
658
+ referer: ref,
659
+ queryReferer: qref,
660
+ rootReferer: qref,
661
+ fromPage: 'video',
662
+ historyLen: '3',
663
+ slots: { before_aid: [['action', action]], after_is_page_visible: [['itemId', itemId]], after_screen: [['secUid', secUid]] },
664
+ });
665
+ return t.requestJson({ method: 'POST', path: '/api/item/collect/', params: p, referer: ref, origin: ORIGIN, body: '', form: true, ticketGuard: true, ticketGuardSecCsrf: false });
666
+ }
667
+ export function followUser(t, userId, secUserId, o) {
668
+ const p = currentParams(t, {
669
+ referer: o.referer,
670
+ queryReferer: o.queryReferer,
671
+ rootReferer: o.queryReferer,
672
+ fromPage: 'video',
673
+ historyLen: '3',
674
+ slots: {
675
+ before_aid: [['action_type', o.actionType]],
676
+ after_channel: [['channel_id', '0']],
677
+ after_focus: [
678
+ ['from', '18'],
679
+ ['fromWeb', '1'],
680
+ ],
681
+ after_from_page: [['from_pre', '0']],
682
+ after_screen: [
683
+ ['sec_user_id', secUserId],
684
+ ['type', o.followType ?? o.actionType],
685
+ ],
686
+ after_tz: [['user_id', userId]],
687
+ },
688
+ });
689
+ return t.requestJson({ method: 'POST', path: '/api/commit/follow/user/', params: p, referer: o.referer, origin: ORIGIN, body: '', form: true, ticketGuard: true });
690
+ }
691
+ export function relatedItems(t, itemId, o = {}) {
692
+ const ref = o.referer ?? `${ORIGIN}/@tiktok/video/${itemId}?lang=zh-Hans`;
693
+ let qref = o.queryReferer;
694
+ if (qref == null) {
695
+ const m = /^(https?:\/\/[^/]+\/@[^/?]+)/.exec(ref);
696
+ qref = m ? `${m[1]}?lang=zh-Hans` : ref;
697
+ }
698
+ const pairs = [
699
+ ['CategoryType', '101'],
700
+ ['WebIdLastTime', t.webIdLastTime],
701
+ ['aid', '1988'],
702
+ ['app_language', 'zh-Hans'],
703
+ ['app_name', 'tiktok_web'],
704
+ ['browser_language', 'zh-CN'],
705
+ ['browser_name', 'Mozilla'],
706
+ ['browser_online', 'true'],
707
+ ['browser_platform', 'Win32'],
708
+ ['browser_version', bversion(t)],
709
+ ['channel', 'tiktok_web'],
710
+ ['clientABVersions', t.clientAbVersions],
711
+ ['cookie_enabled', 'true'],
712
+ ['count', o.count ?? '16'],
713
+ ['coverFormat', '2'],
714
+ ['cursor', o.cursor ?? '0'],
715
+ ['data_collection_enabled', 'true'],
716
+ ['device_id', t.deviceId],
717
+ ['device_platform', 'web_pc'],
718
+ ['focus_state', 'true'],
719
+ ['from_page', 'video'],
720
+ ['history_len', '3'],
721
+ ['isNonPersonalized', 'false'],
722
+ ['is_fullscreen', 'false'],
723
+ ['is_page_visible', 'true'],
724
+ ['itemID', itemId],
725
+ ['language', 'zh-Hans'],
726
+ ['launch_mode', 'direct'],
727
+ ['odinId', t.odinId],
728
+ ['os', 'windows'],
729
+ ['priority_region', t.priorityRegion],
730
+ ['referer', ref],
731
+ ['region', t.region],
732
+ ['root_referer', qref],
733
+ ['screen_height', '1440'],
734
+ ['screen_width', '2560'],
735
+ ['tz_name', 'Asia/Shanghai'],
736
+ ['user_is_login', t.loggedIn ? 'true' : 'false'],
737
+ ['verifyFp', t.verifyFp],
738
+ ['video_encoding', 'dash'],
739
+ ['webcast_language', 'zh-Hans'],
740
+ ];
741
+ return t.requestJson({ method: 'GET', path: '/api/related/item_list/', params: new Params(pairs), referer: ref });
742
+ }
743
+ // ================================================================ 直播
744
+ const LIVE = `${ORIGIN}/live`;
745
+ export function webcastFeed(t, referer = LIVE) {
746
+ const p = currentParams(t, {
747
+ referer,
748
+ queryReferer: '',
749
+ rootReferer: '',
750
+ fromPage: '',
751
+ historyLen: '2',
752
+ slots: {
753
+ after_channel: [
754
+ ['channel_id', '42'],
755
+ ['content_type', '1'],
756
+ ],
757
+ after_cookie: [['cpu_number', '20']],
758
+ after_device_platform: [['device_type', 'web_h264']],
759
+ after_fullscreen: [['is_non_personalized', '0']],
760
+ after_is_page_visible: [['max_time', '0']],
761
+ after_region: [['req_from', 'pc_web_side_follow_default']],
762
+ },
763
+ });
764
+ return t.requestJson({ method: 'GET', path: '/webcast/feed/', params: p, referer });
765
+ }
766
+ export function liveUserRoom(t, uniqueId, referer) {
767
+ const ref = referer ?? `${ORIGIN}/@${uniqueId}/live`;
768
+ const p = currentParams(t, {
769
+ referer: ref,
770
+ queryReferer: '',
771
+ rootReferer: '',
772
+ fromPage: '',
773
+ historyLen: '4',
774
+ includeOdinId: false,
775
+ slots: {
776
+ after_screen: [
777
+ ['sourceType', '54'],
778
+ ['staleTime', '600000'],
779
+ ],
780
+ after_tz: [['uniqueId', uniqueId]],
781
+ },
782
+ });
783
+ return t.requestJson({ method: 'GET', path: '/api-live/user/room', params: p, referer: ref });
784
+ }
785
+ /**
786
+ * 进入直播间(上游 enter_live_room):只有房间号时用它取房间对象(owner、stream_url 等,与 /webcast/feed/ 的房间同结构)。
787
+ * body 是浏览器原样的 form 串。
788
+ */
789
+ export function enterLiveRoom(t, roomId, o = {}) {
790
+ const referer = o.referer ?? LIVE;
791
+ const p = commonParams(t, {
792
+ referer,
793
+ queryReferer: '',
794
+ rootReferer: '',
795
+ historyLen: '4',
796
+ includeFromPage: true,
797
+ fromPage: '',
798
+ includeUserIsLogin: true,
799
+ includeLanguage: false,
800
+ afterDevicePlatform: [['device_type', 'web_h265']],
801
+ });
802
+ const body = `enter_source=${o.enterSource ?? 'recommend-suggested_others_photo'}&room_id=${roomId}`;
803
+ return t.requestJson({ method: 'POST', path: '/webcast/room/enter/', params: p, referer, body, form: true });
804
+ }
805
+ /** 直播间是否在播(上游 check_live_rooms):多个房间号用逗号连接。 */
806
+ export function checkLiveRooms(t, roomIds, referer = LIVE) {
807
+ const ids = Array.isArray(roomIds) ? roomIds.join(',') : roomIds;
808
+ const p = commonParams(t, {
809
+ referer,
810
+ queryReferer: '',
811
+ rootReferer: '',
812
+ historyLen: '2',
813
+ includeFromPage: true,
814
+ fromPage: '',
815
+ includeUserIsLogin: true,
816
+ includeLanguage: false,
817
+ afterRegion: [['room_ids', ids]],
818
+ });
819
+ return t.requestJson({ method: 'GET', path: '/webcast/room/check_alive/', params: p, referer });
820
+ }
821
+ export function webcastDrawerTabs(t, referer = LIVE, scene = '1') {
822
+ const p = currentParams(t, { referer, queryReferer: '', rootReferer: '', fromPage: '', historyLen: '2', slots: { after_root_referer: [['scene', scene]] } });
823
+ return t.requestJson({ method: 'GET', path: '/webcast/feed/drawer_tabs/', params: p, referer, origin: ORIGIN });
824
+ }
825
+ export function liveGiftList(t, roomId, referer = LIVE) {
826
+ const p = currentParams(t, { referer, queryReferer: '', rootReferer: '', fromPage: '', historyLen: '4', includeOdinId: false, slots: { after_region: [['room_id', roomId]] } });
827
+ return t.requestJson({ method: 'GET', path: '/webcast/gift/list/', params: p, referer });
828
+ }
829
+ export function webcastRankList(t, anchorId, roomId, referer = `${ORIGIN}/`) {
830
+ const p = commonParams(t, {
831
+ referer,
832
+ queryReferer: '',
833
+ rootReferer: '',
834
+ historyLen: '2',
835
+ includeFromPage: true,
836
+ fromPage: '',
837
+ includeUserIsLogin: true,
838
+ includeLanguage: false,
839
+ afterAid: [['anchor_id', anchorId]],
840
+ afterRegion: [['room_id', roomId]],
841
+ afterRootReferer: [['source', '0']],
842
+ });
843
+ return t.requestJson({ method: 'GET', path: '/webcast/ranklist/online_audience/', params: p, referer });
844
+ }
845
+ /** 直播 protobuf 拉取(带两个 version_code 的原样 query)。返回原始字节。 */
846
+ export async function webcastImFetch(t, liveId, roomId, o = {}) {
847
+ const p = new Params();
848
+ for (const [k, v] of [
849
+ ['version_code', '180800'],
850
+ ['device_platform', 'web'],
851
+ ['cookie_enabled', 'true'],
852
+ ['screen_width', '2560'],
853
+ ['screen_height', '1440'],
854
+ ['browser_language', 'zh-CN'],
855
+ ['browser_platform', 'Win32'],
856
+ ['browser_name', 'Mozilla'],
857
+ ['browser_version', bversion(t)],
858
+ ['browser_online', 'true'],
859
+ ['tz_name', 'Asia/Shanghai'],
860
+ ['ws_direct', '1'],
861
+ ['aid', '1988'],
862
+ ['app_name', 'tiktok_web'],
863
+ ['live_id', liveId],
864
+ ['version_code', '270000'],
865
+ ['app_language', 'zh-Hans'],
866
+ ['client_enter', '1'],
867
+ ['room_id', roomId],
868
+ ['identity', 'audience'],
869
+ ['history_comment_count', o.historyCommentCount ?? '6'],
870
+ ['fetch_rule', '1'],
871
+ ['last_rtt', '-1'],
872
+ ['cursor', o.cursor ?? '0'],
873
+ ['internal_ext', '0'],
874
+ ['sup_ws_ds_opt', '1'],
875
+ ['resp_content_type', 'protobuf'],
876
+ ['did_rule', '3'],
877
+ ['webcast_language', 'zh-Hans'],
878
+ ]) {
879
+ p.addPair(k, v);
880
+ }
881
+ const res = await t.requestResponse({ method: 'GET', path: '/webcast/im/fetch/', params: p, referer: o.referer ?? LIVE, origin: ORIGIN, accept: '*/*' });
882
+ if (res.status >= 400)
883
+ throw new SpecificAIError('UPSTREAM', `webcast/im/fetch 返回 HTTP ${res.status}`, { detail: { status: res.status } });
884
+ return new Uint8Array(await res.arrayBuffer());
885
+ }
886
+ /** 直播 WS 地址:原样拼接后只把空格换成 %20(上游 _live_ws_url)。 */
887
+ export function liveWsUrl(t, liveId, roomId, marker) {
888
+ if (marker.length !== 16)
889
+ throw new SpecificAIError('ERROR', '直播 WS X-Bogus 长度必须为 16');
890
+ const m = t.metrics;
891
+ const pairs = [
892
+ ['version_code', '180800'],
893
+ ['device_platform', 'web'],
894
+ ['cookie_enabled', 'true'],
895
+ ['screen_width', String(m.screen_width ?? '2560')],
896
+ ['screen_height', String(m.screen_height ?? '1440')],
897
+ ['browser_language', String(m.browser_language ?? 'zh-CN')],
898
+ ['browser_platform', String(m.browser_platform ?? 'Win32')],
899
+ ['browser_name', 'Mozilla'],
900
+ ['browser_version', bversion(t)],
901
+ ['browser_online', 'true'],
902
+ ['tz_name', String(m.tz_name ?? 'Asia/Shanghai')],
903
+ ['app_name', 'tiktok_web'],
904
+ ['sup_ws_ds_opt', '1'],
905
+ ['update_version_code', '2.0.0'],
906
+ ['compress', 'gzip'],
907
+ ['webcast_language', 'zh-Hans'],
908
+ ['ws_direct', '1'],
909
+ ['aid', '1988'],
910
+ ['live_id', liveId],
911
+ ['version_code', '270000'],
912
+ ['app_language', 'zh-Hans'],
913
+ ['client_enter', '1'],
914
+ ['room_id', roomId],
915
+ ['identity', 'audience'],
916
+ ['history_comment_count', '6'],
917
+ ['last_rtt', '0'],
918
+ ['heartbeat_duration', '10000'],
919
+ ['resp_content_type', 'protobuf'],
920
+ ['did_rule', '3'],
921
+ ['X-Bogus', marker],
922
+ ];
923
+ return ('wss://webcast-ws.tiktok.com/webcast/im/ws_proxy/ws_reuse_supplement/?' + pairs.map(([k, v]) => `${k}=${v}`).join('&')).replaceAll(' ', '%20');
924
+ }
925
+ export function postLiveChat(t, roomId, content, o) {
926
+ if (!content)
927
+ throw new SpecificAIError('USAGE', '弹幕内容不能为空');
928
+ if ([...content].length > 150)
929
+ throw new SpecificAIError('USAGE', '弹幕内容不能超过 150 个字符');
930
+ const ts = o.timestamp ?? rand.now();
931
+ const body = compactJson(new Map([
932
+ ['room_id', roomId],
933
+ ['content', content],
934
+ ['emotes_with_index', ''],
935
+ ['input_type', 0],
936
+ ['client_start_timestamp_millisecond', ts],
937
+ ]));
938
+ const p = currentParams(t, {
939
+ referer: o.referer,
940
+ queryReferer: '',
941
+ rootReferer: '',
942
+ fromPage: '',
943
+ historyLen: o.historyLen ?? '6',
944
+ includeOdinId: false,
945
+ slots: {
946
+ after_channel: [
947
+ ['client_start_timestamp_millisecond', ts],
948
+ ['content', content],
949
+ ],
950
+ after_device_platform: [['emotes_with_index', '']],
951
+ after_history: [['input_type', 0]],
952
+ after_region: [['room_id', roomId]],
953
+ },
954
+ });
955
+ return t.requestJson({
956
+ method: 'POST',
957
+ path: '/webcast/room/chat/',
958
+ params: p,
959
+ referer: o.referer,
960
+ origin: ORIGIN,
961
+ body,
962
+ extraHeaders: { 'content-type': 'application/json; charset=UTF-8' },
963
+ ticketGuard: true,
964
+ ticketGuardSecCsrf: true,
965
+ ticketGuardTtCsrfHeader: false,
966
+ allowEmpty: true,
967
+ });
968
+ }
969
+ export function postLiveLike(t, toUid, roomId, o) {
970
+ const body = compactJson(new Map([
971
+ ['to_uid', toUid],
972
+ ['count', o.count ?? 1],
973
+ ['room_id', roomId],
974
+ ['enter_from', 'live'],
975
+ ]));
976
+ const p = currentParams(t, { referer: o.referer, queryReferer: '', rootReferer: '', fromPage: '', historyLen: '6', includeOdinId: false });
977
+ return t.requestJson({
978
+ method: 'POST',
979
+ path: '/webcast/room/like/',
980
+ params: p,
981
+ referer: o.referer,
982
+ origin: ORIGIN,
983
+ body,
984
+ extraHeaders: { 'content-type': 'application/json; charset=UTF-8' },
985
+ contentTypeBeforeMobile: true,
986
+ allowEmpty: true,
987
+ });
988
+ }
989
+ // ================================================================ 页面(hydration / SSR)
990
+ export function userHtml(t, url) {
991
+ return t.document(url, url);
992
+ }
993
+ /** 视频页 hydration 的 itemStruct(上游 get_video_detail)。 */
994
+ export async function videoDetail(t, url, expectedId) {
995
+ const html = await userHtml(t, url);
996
+ const detail = hydration(html)?.['webapp.video-detail'];
997
+ const item = detail?.itemInfo?.itemStruct;
998
+ if (!item?.id) {
999
+ const status = detail?.statusCode;
1000
+ if (status === 10204 || status === 10216)
1001
+ throw new SpecificAIError('UPSTREAM', `作品不存在或不可见`, { detail: { statusCode: status, statusMsg: detail?.statusMsg } });
1002
+ throw new SpecificAIError('UPSTREAM', '视频页 hydration 中没有 webapp.video-detail.itemInfo.itemStruct', { detail: { statusCode: status } });
1003
+ }
1004
+ if (expectedId && String(item.id) !== expectedId)
1005
+ throw new SpecificAIError('UPSTREAM', `视频页的作品 ID 与链接不一致:期望 ${expectedId},实际 ${item.id}`);
1006
+ return item;
1007
+ }
1008
+ /** 商品页 SSR 的 product_info.component_data(上游 get_shop_product_detail)。 */
1009
+ export async function shopProductDetail(t, productUrl, productId) {
1010
+ const html = await shopProductHtml(t, productUrl);
1011
+ if (/<title>Security Check<\/title>/i.test(html)) {
1012
+ throw new SpecificAIError('RISK_CONTROL', 'TikTok Shop 要求人机验证', { detail: { kind: 'captcha' }, hint: '在浏览器打开商品页通过验证后,重新导出 Cookie 登录' });
1013
+ }
1014
+ if (/region_not_match/.test(html))
1015
+ throw new SpecificAIError('UPSTREAM', '商品在当前地区不可用', { detail: { url: productUrl } });
1016
+ const m = /<script[^>]*id="__MODERN_ROUTER_DATA__"[^>]*>([\s\S]*?)<\/script>/.exec(html);
1017
+ if (!m)
1018
+ throw new SpecificAIError('UPSTREAM', 'TikTok Shop 页面中没有 __MODERN_ROUTER_DATA__');
1019
+ let loader;
1020
+ try {
1021
+ loader = jsonLoads(m[1]).loaderData;
1022
+ }
1023
+ catch {
1024
+ throw new SpecificAIError('UPSTREAM', 'TikTok Shop SSR loaderData 无法解析');
1025
+ }
1026
+ let data = null;
1027
+ for (const route of Object.values(loader ?? {})) {
1028
+ const comp = route?.page_config?.components_map?.find?.((c) => c?.component_name === 'product_info');
1029
+ if (comp) {
1030
+ data = comp.component_data;
1031
+ break;
1032
+ }
1033
+ }
1034
+ if (!data || typeof data !== 'object')
1035
+ throw new SpecificAIError('UPSTREAM', 'TikTok Shop SSR 中没有 product_info.component_data');
1036
+ const pid = String(data.product_info?.product_model?.product_id ?? '');
1037
+ if (pid !== productId)
1038
+ throw new SpecificAIError('UPSTREAM', `TikTok Shop 商品 ID 与链接不一致:期望 ${productId},实际 ${pid}`);
1039
+ return data;
1040
+ }
1041
+ /** 商品页(Chrome 直接导航的请求头顺序)。 */
1042
+ export async function shopProductHtml(t, productUrl) {
1043
+ const headers = [
1044
+ ['upgrade-insecure-requests', '1'],
1045
+ ['user-agent', t.ua],
1046
+ ['sec-ch-ua', t.browser.secChUa],
1047
+ ['sec-ch-ua-mobile', '?0'],
1048
+ ['sec-ch-ua-platform', t.browser.secChUaPlatform],
1049
+ ['accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7'],
1050
+ ['accept-encoding', 'gzip, deflate, br, zstd'],
1051
+ ['accept-language', t.browser.acceptLanguage],
1052
+ ['priority', 'u=0, i'],
1053
+ ['sec-fetch-dest', 'document'],
1054
+ ['sec-fetch-mode', 'navigate'],
1055
+ ['sec-fetch-site', 'none'],
1056
+ ['sec-fetch-user', '?1'],
1057
+ ];
1058
+ const res = await t.send({ url: productUrl, headers });
1059
+ if (res.status >= 400)
1060
+ throw new SpecificAIError('UPSTREAM', `TikTok Shop 页面返回 HTTP ${res.status}`, { detail: { status: res.status } });
1061
+ return res.text();
1062
+ }
1063
+ /** 评价翻页:页 1 来自商品页 SSR,从第 2 页开始走这里,每页新算 BSID。 */
1064
+ export async function shopReviewPage(t, productUrl, productId, pageStart) {
1065
+ if (pageStart < 2)
1066
+ throw new SpecificAIError('ERROR', 'page_start=1 来自商品页 SSR,翻页接口从 2 开始');
1067
+ const body = compactJson(new Map([
1068
+ ['product_id', productId],
1069
+ ['page_start', pageStart],
1070
+ ['page_size', 3],
1071
+ ['sort_rule', 1],
1072
+ ['review_filter', new Map([['filter_type', 1], ['filter_value', 6]])],
1073
+ ['component_name', 'pdp_left_reviews'],
1074
+ ]));
1075
+ const endpoint = `${SHOP}/api/shop/pdp_desktop/get_product_reviews`;
1076
+ const cookie = t.device.document_cookie || t.cookieStr;
1077
+ const signed = await shopSign({
1078
+ url: endpoint,
1079
+ method: 'POST',
1080
+ headers: [
1081
+ ['accept', 'application/json,*/*;q=0.8'],
1082
+ ['content-type', 'application/json'],
1083
+ ],
1084
+ body,
1085
+ cookie,
1086
+ userAgent: t.ua,
1087
+ xBogus: (msToken) => encodeXBogus(`msToken=${msToken}`, t.ua, body, { ubcode: 14, magic: 2894886431 }),
1088
+ });
1089
+ const m = /^https:\/\/shop\.tiktok\.com\/api\/shop\/pdp_desktop\/get_product_reviews\?msToken=([^&]+)&X-Bogus=([^&]+)&_signature=([^&]+)&X-Tts-Oec-Bsid=([0-9a-f]{382})$/.exec(signed.signed_url);
1090
+ if (!m || m[2].length !== 28 || m[3].length !== 47 || m[4] !== signed.bsid)
1091
+ throw new SpecificAIError('ERROR', 'Shop 四字段签名结果未通过校验');
1092
+ const headers = [
1093
+ ['sec-ch-ua-platform', t.browser.secChUaPlatform],
1094
+ ['referer', productUrl],
1095
+ ['user-agent', t.ua],
1096
+ ['accept', 'application/json,*/*;q=0.8'],
1097
+ ['sec-ch-ua', t.browser.secChUa],
1098
+ ['content-type', 'application/json'],
1099
+ ['sec-ch-ua-mobile', '?0'],
1100
+ ['accept-encoding', 'gzip, deflate, br, zstd'],
1101
+ ['accept-language', t.browser.acceptLanguage],
1102
+ ['content-length', String(Buffer.byteLength(body))],
1103
+ ['origin', SHOP],
1104
+ ['priority', 'u=1, i'],
1105
+ ['sec-fetch-dest', 'empty'],
1106
+ ['sec-fetch-mode', 'cors'],
1107
+ ['sec-fetch-site', 'same-origin'],
1108
+ ];
1109
+ const res = await t.send({ method: 'POST', url: signed.signed_url, headers, body });
1110
+ const result = await t.json(res, { path: '/api/shop/pdp_desktop/get_product_reviews' });
1111
+ if (result.code !== 0 || !result.data || typeof result.data !== 'object') {
1112
+ throw new SpecificAIError('UPSTREAM', `TikTok Shop 评价接口返回错误:${result.message ?? result.code}`, { detail: { code: result.code, message: result.message } });
1113
+ }
1114
+ return result.data;
1115
+ }
1116
+ // ================================================================ 创作者中心:作品列表、地点
1117
+ export function creatorItemList(t, cursor = 0, size = 50) {
1118
+ if (cursor < 0 || size < 1 || size > 50)
1119
+ throw new SpecificAIError('USAGE', 'Creator 作品列表需要 cursor >= 0 且 1 <= size <= 50');
1120
+ if (!t.ttCsrfToken)
1121
+ throw new SpecificAIError('AUTH_REQUIRED', 'Creator 作品列表缺少 tt-csrf-token(cookie tt_csrf_token)', { hint: 'specific-ai tiktok auth login --cookie @tiktok-session.json' });
1122
+ const country = String(t.cookie('store-country-code') || t.region).toUpperCase();
1123
+ const p = new Params([
1124
+ ['locale', 'zh-Hans'],
1125
+ ['aid', '1988'],
1126
+ ['priority_region', country],
1127
+ ['region', country],
1128
+ ['tz_name', 'Asia/Shanghai'],
1129
+ ['app_name', 'tiktok_creator_center'],
1130
+ ['app_language', 'zh-Hans'],
1131
+ ['device_platform', 'web_pc'],
1132
+ ['channel', 'tiktok_web'],
1133
+ ['device_id', t.deviceId],
1134
+ ['os', 'win'],
1135
+ ['screen_width', '2560'],
1136
+ ['screen_height', '1440'],
1137
+ ['browser_language', 'zh-CN'],
1138
+ ['browser_platform', 'Win32'],
1139
+ ['browser_name', 'Mozilla'],
1140
+ ['browser_version', bversion(t)],
1141
+ ], new Set(['browser_version']));
1142
+ const body = compactJson(new Map([
1143
+ ['cursor', cursor],
1144
+ ['size', size],
1145
+ ['query', new Map([['sort_orders', [new Map([['field_name', 'post_time'], ['order', 2]])]], ['conditions', []], ['is_recent_posts', false]])],
1146
+ ]));
1147
+ return t.requestJson({
1148
+ method: 'POST',
1149
+ path: '/tiktok/creator/manage/item_list/v1/',
1150
+ params: p,
1151
+ referer: `${ORIGIN}/tiktokstudio/content`,
1152
+ body,
1153
+ origin: ORIGIN,
1154
+ accept: AXIOS_ACCEPT,
1155
+ extraHeaders: { 'agw-js-conv': 'str', 'tt-csrf-token': t.ttCsrfToken },
1156
+ headerOrder: [
1157
+ 'sec-ch-ua-platform', 'referer', 'sec-ch-ua', 'sec-ch-ua-mobile', 'agw-js-conv', 'tt-csrf-token', 'user-agent', 'accept', 'content-type',
1158
+ 'accept-encoding', 'accept-language', 'content-length', 'cookie', 'origin', 'priority', 'sec-fetch-dest', 'sec-fetch-mode', 'sec-fetch-site',
1159
+ ],
1160
+ });
1161
+ }
1162
+ export const UPLOAD_REFERER = `${ORIGIN}/tiktokstudio/upload?from=webapp&lang=zh-Hans&tab=video`;
1163
+ export function creatorPoiList(t, o = {}) {
1164
+ const webParams = new Map([
1165
+ ['aid', 1988],
1166
+ ['app_name', 'tiktok_web'],
1167
+ ['channel', 'tiktok_web'],
1168
+ ['device_platform', 'web_pc'],
1169
+ ['device_id', t.deviceId],
1170
+ ['region', t.region],
1171
+ ['priority_region', t.priorityRegion],
1172
+ ['referer', ''],
1173
+ ['cookie_enabled', true],
1174
+ ['screen_width', 2560],
1175
+ ['screen_height', 1440],
1176
+ ['browser_language', 'zh-CN'],
1177
+ ['browser_platform', 'Win32'],
1178
+ ['browser_name', 'Mozilla'],
1179
+ ['browser_version', bversion(t)],
1180
+ ['browser_online', true],
1181
+ ['verifyFp', t.verifyFp],
1182
+ ['app_language', 'zh-Hans'],
1183
+ ['webcast_language', 'zh-Hans'],
1184
+ ['tz_name', 'Asia/Shanghai'],
1185
+ ['is_fullscreen', false],
1186
+ ['history_len', o.historyLen ?? '20'],
1187
+ ]);
1188
+ const body = compactJson(new Map([
1189
+ ['page_num', o.pageNum ?? 1],
1190
+ ['page_size', o.pageSize ?? 10],
1191
+ ['search_params', new Map([['search_id', o.searchId ?? ''], ['creation_id', o.creationId ?? ''], ['last_search_id', o.lastSearchId ?? '']])],
1192
+ ['web_params', webParams],
1193
+ ]));
1194
+ return t.requestJson({ method: 'POST', path: '/tiktok/v1/creator/poi/list', params: new Params([['aid', '1988']]), referer: UPLOAD_REFERER, body, signed: false, accept: AXIOS_ACCEPT, origin: ORIGIN });
1195
+ }
1196
+ // ================================================================ 私信的 HTTP 部分
1197
+ /** privacy config 里的 wid(私信 WS 的 access_key 从它算)。 */
1198
+ export function cookiePrivacyConfig(t, referer = `${ORIGIN}/`) {
1199
+ const p = new Params([
1200
+ ['locale', 'zh-Hans'],
1201
+ ['appId', '1988'],
1202
+ ['theme', 'default'],
1203
+ ['tea', '1'],
1204
+ ]);
1205
+ return t.requestJson({ method: 'GET', path: '/api/v1/web-cookie-privacy/config', params: p, referer, signed: false, accept: AXIOS_ACCEPT });
1206
+ }
1207
+ /** 私信 protobuf 请求(im-api-<region>.tiktok.com)。 */
1208
+ export async function postImProtobuf(t, path, raw, referer = `${ORIGIN}/`) {
1209
+ const headers = [
1210
+ ['sec-ch-ua-platform', t.browser.secChUaPlatform],
1211
+ ['referer', referer],
1212
+ ['user-agent', t.ua],
1213
+ ['accept', 'application/x-protobuf'],
1214
+ ['sec-ch-ua', t.browser.secChUa],
1215
+ ['content-type', 'application/x-protobuf'],
1216
+ ['sec-ch-ua-mobile', '?0'],
1217
+ ['accept-encoding', 'gzip, deflate, br, zstd'],
1218
+ ['accept-language', t.browser.acceptLanguage],
1219
+ ['content-length', String(raw.length)],
1220
+ ['origin', ORIGIN],
1221
+ ['priority', 'u=1, i'],
1222
+ ['sec-fetch-dest', 'empty'],
1223
+ ['sec-fetch-mode', 'cors'],
1224
+ ['sec-fetch-site', 'same-site'],
1225
+ ];
1226
+ const url = `https://im-api-${t.region.toLowerCase()}.tiktok.com${path}`;
1227
+ const res = await t.send({ method: 'POST', url, headers, body: raw });
1228
+ if (res.status >= 400)
1229
+ throw new SpecificAIError('UPSTREAM', `私信接口返回 HTTP ${res.status}`, { detail: { status: res.status, path } });
1230
+ const bytes = new Uint8Array(await res.arrayBuffer());
1231
+ if (!bytes.length)
1232
+ throw new SpecificAIError('UPSTREAM', '私信 protobuf 响应为空');
1233
+ return bytes;
1234
+ }