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,805 @@
1
+ import { crc32, deflateRawSync } from 'node:zlib';
2
+ import { SpecificAIError } from '../../../core/errors.js';
3
+ import { mp4VideoTrack } from '../../../core/mp4.js';
4
+ import { compactJson } from '../../../core/py.js';
5
+ import * as rand from '../../../core/rand.js';
6
+ import { UPLOAD_REFERER } from './api.js';
7
+ import { Headers, ORIGIN, Params } from './profile.js';
8
+ import { signAwsV4, ticketTimestamp } from './sign.js';
9
+ /**
10
+ * TikTok Studio 的上传与发布(上游 api/tiktok_web.py 的 get_upload_auth … post_project)。
11
+ * 媒体元数据:上游用 ffmpeg / Pillow,specific-ai 只依赖 Node——MP4 头在本地解析,封面图由调用方给出,
12
+ * 图片编解码用 @napi-rs/canvas。
13
+ */
14
+ const AXIOS_ACCEPT = 'application/json, text/plain, */*';
15
+ const PHOTO_REFERER = `${ORIGIN}/tiktokstudio/upload/post/photo`;
16
+ export const PROJECT_POST_PATH = '/tiktok/web/project/post/v1/';
17
+ const LOWER_DIGITS = 'abcdefghijklmnopqrstuvwxyz0123456789';
18
+ const LETTERS_DIGITS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
19
+ /** 浏览器 11 位上传随机串(上游 _upload_random_s)。 */
20
+ export const uploadRandomS = () => rand.string(11, LOWER_DIGITS);
21
+ export const creationId = () => 'ROO_' + rand.string(17, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_');
22
+ export const photoCreationId = () => rand.string(21, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_');
23
+ /** `secrets.randbelow(n)`。 */
24
+ const randbelow = (n) => Math.floor(rand.random() * n);
25
+ // ================================================================ STS 凭证
26
+ export function uploadAuth(t, o = {}) {
27
+ const signed = o.signed ?? true;
28
+ return t.requestJson({
29
+ method: 'GET',
30
+ path: '/api/v1/video/upload/auth/',
31
+ params: new Params([['aid', '1988']]),
32
+ referer: o.referer ?? `${ORIGIN}/tiktokstudio/upload?from=webapp&tab=video`,
33
+ signed,
34
+ accept: AXIOS_ACCEPT,
35
+ headerOrder: ['sec-ch-ua-platform', 'referer', 'user-agent', 'accept', 'sec-ch-ua', 'sec-ch-ua-mobile', 'accept-encoding', 'accept-language', 'cookie', 'priority', 'sec-fetch-dest', 'sec-fetch-mode', 'sec-fetch-site'],
36
+ });
37
+ }
38
+ function credentials(auth, tokenName) {
39
+ const c = auth?.[tokenName];
40
+ const missing = ['access_key_id', 'secret_acess_key', 'session_token', 'space_name'].filter((k) => !c?.[k]);
41
+ if (missing.length)
42
+ throw new SpecificAIError('UPSTREAM', `upload/auth 的 ${tokenName} 缺少字段:${missing.join(', ')}`);
43
+ return c;
44
+ }
45
+ const tokenName = (space) => (space === 'tiktok-ai-frame' ? 'vframe_token_v5' : 'video_token_v5');
46
+ /** /top/v1 的请求头:AWS V4 签名字段插在浏览器的固定位置(上游 _vod_headers)。 */
47
+ function vodHeaders(t, o) {
48
+ const body = o.body ?? new Uint8Array();
49
+ const post = o.method.toUpperCase() === 'POST';
50
+ const aws = signAwsV4({
51
+ method: o.method,
52
+ path: '/top/v1',
53
+ query: o.params.pairs,
54
+ accessKeyId: o.creds.access_key_id,
55
+ secretAccessKey: o.creds.secret_acess_key,
56
+ sessionToken: o.creds.session_token,
57
+ body,
58
+ service: o.service ?? 'vod',
59
+ amzDate: undefined,
60
+ });
61
+ const base = t.headers(post ? 'POST' : 'GET', { referer: o.referer, origin: post ? ORIGIN : '', contentLength: post ? String(body.length) : null, secFetchSite: 'same-origin' });
62
+ if (post)
63
+ base.set('content-type', o.contentType ?? 'text/plain;charset=UTF-8');
64
+ base.set('accept', '*/*');
65
+ const h = new Headers();
66
+ if (body.length)
67
+ h.set('x-amz-content-sha256', aws['x-amz-content-sha256']);
68
+ h.set('sec-ch-ua-platform', base.get('sec-ch-ua-platform'));
69
+ h.set('authorization', aws.authorization);
70
+ h.set('referer', base.get('referer'));
71
+ h.set('sec-ch-ua', base.get('sec-ch-ua'));
72
+ h.set('sec-ch-ua-mobile', base.get('sec-ch-ua-mobile'));
73
+ h.set('x-amz-security-token', aws['x-amz-security-token']);
74
+ h.set('x-amz-date', aws['x-amz-date']);
75
+ h.set('user-agent', base.get('user-agent'));
76
+ if (post)
77
+ h.set('content-type', base.get('content-type'));
78
+ h.set('accept', base.get('accept'));
79
+ for (const k of ['accept-encoding', 'accept-language', 'content-length', 'cookie', 'origin', 'priority', 'sec-fetch-dest', 'sec-fetch-mode', 'sec-fetch-site']) {
80
+ if (base.has(k))
81
+ h.set(k, base.get(k));
82
+ }
83
+ return h.pairs();
84
+ }
85
+ async function topV1(t, o) {
86
+ const headers = vodHeaders(t, o);
87
+ const res = await t.send({ method: o.method, url: `${ORIGIN}/top/v1?${o.params.toQuery()}`, headers, body: o.body });
88
+ return t.json(res, { path: '/top/v1' });
89
+ }
90
+ // ================================================================ 图文(ImageX)
91
+ export function applyImageUpload(t, fileSize, auth, o = {}) {
92
+ if (fileSize <= 0)
93
+ throw new SpecificAIError('USAGE', '图片不能为空');
94
+ const s = o.s ?? uploadRandomS();
95
+ const params = new Params([
96
+ ['Action', 'ApplyImageUpload'],
97
+ ['Version', '2018-08-01'],
98
+ ['ServiceId', 'photomode'],
99
+ ['FileSize', String(fileSize)],
100
+ ['s', s],
101
+ ['device_platform', 'web'],
102
+ ]);
103
+ return topV1(t, { method: 'GET', params, creds: credentials(auth, 'video_token_v5'), referer: o.referer ?? PHOTO_REFERER, service: 'imagex' });
104
+ }
105
+ export function commitImageUpload(t, sessionKey, auth, referer = PHOTO_REFERER) {
106
+ const body = new TextEncoder().encode(compactJson({ SessionKey: sessionKey }));
107
+ const params = new Params([
108
+ ['Action', 'CommitImageUpload'],
109
+ ['Version', '2018-08-01'],
110
+ ['ServiceId', 'photomode'],
111
+ ]);
112
+ return topV1(t, { method: 'POST', params, creds: credentials(auth, 'video_token_v5'), referer, body, service: 'imagex', contentType: 'application/json' });
113
+ }
114
+ /** ApplyImageUpload → TOS 原样上传 → CommitImageUpload。 */
115
+ export async function uploadPhotoBytes(t, data, auth, o = {}) {
116
+ const referer = o.referer ?? PHOTO_REFERER;
117
+ const applied = await applyImageUpload(t, data.length, auth, { referer });
118
+ const uploaded = await uploadTosBytes(t, applied, data, { userId: o.userId, filename: 'undefined' });
119
+ const sessionKey = applied?.Result?.InnerUploadAddress?.UploadNodes?.[0]?.SessionKey;
120
+ if (!sessionKey)
121
+ throw new SpecificAIError('UPSTREAM', 'ApplyImageUpload 缺少 SessionKey');
122
+ const committed = await commitImageUpload(t, String(sessionKey), auth, referer);
123
+ const plugin = committed?.Result?.PluginResult?.[0] ?? {};
124
+ const uri = String(plugin.ImageUri ?? '');
125
+ const width = Number(plugin.ImageWidth);
126
+ const height = Number(plugin.ImageHeight);
127
+ if (!uri || !(width > 0) || !(height > 0))
128
+ throw new SpecificAIError('UPSTREAM', 'CommitImageUpload 没有返回图片 URI / 尺寸');
129
+ return { apply: applied, upload: uploaded, commit: committed, uri, width, height };
130
+ }
131
+ // ================================================================ 视频(VOD)
132
+ export function uploadCandidates(t, auth, referer = UPLOAD_REFERER) {
133
+ const params = new Params([
134
+ ['Action', 'GetUploadCandidates'],
135
+ ['Version', '2020-11-19'],
136
+ ['SpaceName', 'tiktok'],
137
+ ['X-Amz-Expires', '604800'],
138
+ ]);
139
+ return topV1(t, { method: 'GET', params, creds: credentials(auth, 'video_token_v5'), referer });
140
+ }
141
+ /** 四个 1 MiB 测速请求,返回按耗时排序的上传节点(上游 probe_upload_candidates)。 */
142
+ export async function probeUploadCandidates(t, candidates) {
143
+ const domains = candidates?.Result?.Domains ?? [];
144
+ if (!domains.length)
145
+ throw new SpecificAIError('UPSTREAM', 'GetUploadCandidates 返回空 Domains');
146
+ const payload = new Uint8Array(1048576);
147
+ const probe = async (d) => {
148
+ const host = String(d.Name ?? '');
149
+ const store = String(d.StoreID ?? '');
150
+ const ticket = String(d.Sign ?? '');
151
+ if (!host || !store || !ticket)
152
+ throw new SpecificAIError('UPSTREAM', 'GetUploadCandidates Domain 字段为空');
153
+ const headers = [
154
+ ['sec-ch-ua-platform', t.browser.secChUaPlatform],
155
+ ['authorization', ticket],
156
+ ['referer', `${ORIGIN}/`],
157
+ ['sec-ch-ua', t.browser.secChUa],
158
+ ['content-crc32', 'ignore'],
159
+ ['sec-ch-ua-mobile', '?0'],
160
+ ['user-agent', t.ua],
161
+ ['content-type', 'application/octet-stream'],
162
+ ['content-disposition', 'attachment; filename="undefined"'],
163
+ ['accept', '*/*'],
164
+ ['accept-encoding', 'gzip, deflate, br, zstd'],
165
+ ['accept-language', t.browser.acceptLanguage],
166
+ ['content-length', String(payload.length)],
167
+ ['origin', ORIGIN],
168
+ ['sec-fetch-dest', 'empty'],
169
+ ['sec-fetch-mode', 'cors'],
170
+ ['sec-fetch-site', 'cross-site'],
171
+ ];
172
+ const started = performance.now();
173
+ const res = await t.send({ method: 'POST', url: `https://${host}/upload/v1/${store}?speedtest`, headers, body: payload, cookies: false });
174
+ if (res.status >= 400)
175
+ throw new SpecificAIError('UPSTREAM', `上传节点测速失败:HTTP ${res.status}`, { detail: { host } });
176
+ await res.arrayBuffer();
177
+ return [performance.now() - started, host];
178
+ };
179
+ const ranked = await Promise.all(domains.slice(0, 4).map(probe));
180
+ return ranked.sort((a, b) => a[0] - b[0]).map(([, h]) => h);
181
+ }
182
+ export async function applyUploadInner(t, fileSize, o) {
183
+ const fileType = o.fileType ?? 'video';
184
+ const space = o.spaceName ?? 'tiktok';
185
+ const referer = o.referer ?? UPLOAD_REFERER;
186
+ const creds = credentials(o.auth, tokenName(space));
187
+ if (creds.space_name !== space)
188
+ throw new SpecificAIError('UPSTREAM', `${tokenName(space)}.space_name 与 SpaceName 不一致`);
189
+ const s = o.s ?? uploadRandomS();
190
+ let hosts = o.clientBestHosts;
191
+ if (fileType === 'video' && hosts == null)
192
+ hosts = await probeUploadCandidates(t, await uploadCandidates(t, o.auth, referer));
193
+ const pairs = [
194
+ ['Action', 'ApplyUploadInner'],
195
+ ['Version', '2020-11-19'],
196
+ ['SpaceName', space],
197
+ ['FileType', fileType],
198
+ ['IsInner', o.isInner ?? '1'],
199
+ ];
200
+ if (hosts != null) {
201
+ const joined = typeof hosts === 'string' ? hosts : hosts.join(',');
202
+ if (!joined)
203
+ throw new SpecificAIError('UPSTREAM', 'ApplyUploadInner 的 ClientBestHosts 不能为空');
204
+ pairs.push(['ClientBestHosts', joined]);
205
+ }
206
+ if (o.includeFileSize ?? true)
207
+ pairs.push(['FileSize', String(fileSize)]);
208
+ if (fileType === 'video')
209
+ pairs.push(['X-Amz-Expires', o.xAmzExpires ?? '604800']);
210
+ pairs.push(['s', s]);
211
+ if (o.scene != null)
212
+ pairs.push(['Scene', o.scene]);
213
+ pairs.push(['device_platform', 'web']);
214
+ const tag = o.businessTag ?? (fileType === 'video' ? 'tiktok_video_submission_web' : undefined);
215
+ if (tag != null)
216
+ pairs.push(['business_tag', tag]);
217
+ return topV1(t, { method: 'GET', params: new Params(pairs), creds, referer });
218
+ }
219
+ function storeInfo(applied) {
220
+ const node = applied?.Result?.InnerUploadAddress?.UploadNodes?.[0];
221
+ const store = node?.StoreInfos?.[0];
222
+ if (!node?.UploadHost || !store?.StoreUri || !store?.Auth)
223
+ throw new SpecificAIError('UPSTREAM', 'ApplyUploadInner 缺少 InnerUploadAddress.UploadNodes/StoreInfos');
224
+ return { host: String(node.UploadHost), node };
225
+ }
226
+ /** 按 ApplyUploadInner 给的节点直传;视频走带 post_upload_req 的 multipart。 */
227
+ export async function uploadTosBytes(t, applied, data, o = {}) {
228
+ const { host, node } = storeInfo(applied);
229
+ const store = node.StoreInfos[0];
230
+ const userId = String(o.userId || t.odinId);
231
+ if (!userId)
232
+ throw new SpecificAIError('UPSTREAM', 'TOS 上传缺少 x-storage-u');
233
+ let payload = data;
234
+ let contentType = 'application/octet-stream';
235
+ if (o.postUpload) {
236
+ if (!o.auth)
237
+ throw new SpecificAIError('ERROR', '视频 post-upload multipart 缺少本次 upload/auth');
238
+ const creds = credentials(o.auth, 'video_token_v5');
239
+ const sessionKey = String(node.SessionKey ?? '');
240
+ if (!sessionKey)
241
+ throw new SpecificAIError('UPSTREAM', '视频 post-upload 缺少 UploadNode.SessionKey');
242
+ const boundary = o.boundary ?? '----WebKitFormBoundary' + rand.string(16, LETTERS_DIGITS);
243
+ const postBody = compactJson(new Map([
244
+ ['sts2_token', creds.session_token],
245
+ ['sts2_secret', creds.secret_acess_key],
246
+ ['session_key', sessionKey],
247
+ ['functions', o.functions ?? []],
248
+ ]));
249
+ payload = Buffer.concat([
250
+ Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="blob"\r\nContent-Type: application/octet-stream\r\n\r\n`, 'ascii'),
251
+ data,
252
+ Buffer.from(`\r\n--${boundary}\r\nContent-Disposition: form-data; name="post_upload_req"\r\n\r\n`, 'ascii'),
253
+ Buffer.from(postBody, 'utf8'),
254
+ Buffer.from(`\r\n--${boundary}--\r\n`, 'ascii'),
255
+ ]);
256
+ contentType = `multipart/form-data; boundary=${boundary}`;
257
+ }
258
+ const headers = [
259
+ ['sec-ch-ua-platform', t.browser.secChUaPlatform],
260
+ ['authorization', String(store.Auth)],
261
+ ['referer', `${ORIGIN}/`],
262
+ ['sec-ch-ua', t.browser.secChUa],
263
+ ['content-crc32', (crc32(data) >>> 0).toString(16).padStart(8, '0')],
264
+ ['sec-ch-ua-mobile', '?0'],
265
+ ...(o.postUpload ? [['x-upload-with-postupload', '1']] : []),
266
+ ['user-agent', t.ua],
267
+ ['x-storage-u', userId],
268
+ ['content-type', contentType],
269
+ ...(!o.postUpload ? [['content-disposition', `attachment; filename="${o.filename ?? 'undefined'}"`]] : []),
270
+ ['accept', '*/*'],
271
+ ['accept-encoding', 'gzip, deflate, br, zstd'],
272
+ ['accept-language', t.browser.acceptLanguage],
273
+ ['content-length', String(payload.length)],
274
+ ['origin', ORIGIN],
275
+ ['sec-fetch-dest', 'empty'],
276
+ ['sec-fetch-mode', 'cors'],
277
+ ['sec-fetch-site', 'cross-site'],
278
+ ];
279
+ const res = await t.send({ method: 'POST', url: `https://${host}/upload/v1/${store.StoreUri}`, headers, body: payload, cookies: false });
280
+ return t.json(res, { path: '/upload/v1' });
281
+ }
282
+ export function commitUploadInner(t, sessionKey, o) {
283
+ const body = new TextEncoder().encode(compactJson(new Map([['SessionKey', sessionKey], ['Functions', o.functions ?? []]])));
284
+ const space = o.spaceName ?? 'tiktok';
285
+ const params = new Params([
286
+ ['Action', 'CommitUploadInner'],
287
+ ['Version', '2020-11-19'],
288
+ ['SpaceName', space],
289
+ ]);
290
+ return topV1(t, { method: 'POST', params, creds: credentials(o.auth, tokenName(space)), referer: o.referer ?? UPLOAD_REFERER, body });
291
+ }
292
+ /** 视频:Apply → multipart post-upload(一步提交);图片:Apply → 直传 → CommitUploadInner。 */
293
+ export async function uploadMediaBytes(t, data, o) {
294
+ const fileType = o.fileType ?? 'video';
295
+ const applied = await applyUploadInner(t, data.length, {
296
+ fileType,
297
+ spaceName: o.spaceName,
298
+ scene: o.scene,
299
+ businessTag: o.businessTag,
300
+ auth: o.auth,
301
+ clientBestHosts: o.clientBestHosts,
302
+ referer: o.referer,
303
+ });
304
+ const uploaded = await uploadTosBytes(t, applied, data, { userId: o.userId, filename: o.filename, auth: o.auth, postUpload: fileType === 'video', functions: o.functions });
305
+ const node = applied?.Result?.InnerUploadAddress?.UploadNodes?.[0];
306
+ const videoId = String(node?.Vid ?? '');
307
+ if (!videoId)
308
+ throw new SpecificAIError('UPSTREAM', 'ApplyUploadInner 缺少 UploadNode.Vid');
309
+ if (fileType === 'video')
310
+ return { apply: applied, upload: uploaded, video_id: videoId };
311
+ const sessionKey = String(node?.SessionKey ?? '');
312
+ if (!sessionKey)
313
+ throw new SpecificAIError('UPSTREAM', '图片 ApplyUploadInner 缺少 SessionKey');
314
+ const committed = await commitUploadInner(t, sessionKey, { functions: o.functions, spaceName: o.spaceName, auth: o.auth, referer: o.referer });
315
+ return { apply: applied, upload: uploaded, commit: committed, video_id: videoId };
316
+ }
317
+ // ================================================================ 转码
318
+ /** SecSDK 的 HEAD 引导:拿 x-ware-csrf-token 的第 2 段作为 x-secsdk-csrf-token。 */
319
+ export async function bootstrapTranscodeCsrf(t, referer = UPLOAD_REFERER) {
320
+ const base = t.headers('GET', { referer, secFetchSite: 'same-origin' });
321
+ const h = new Headers();
322
+ h.set('x-secsdk-csrf-request', '1');
323
+ h.set('sec-ch-ua-platform', base.get('sec-ch-ua-platform'));
324
+ h.set('referer', base.get('referer'));
325
+ h.set('user-agent', base.get('user-agent'));
326
+ h.set('x-secsdk-csrf-version', '1.2.22');
327
+ h.set('sec-ch-ua', base.get('sec-ch-ua'));
328
+ h.set('sec-ch-ua-mobile', base.get('sec-ch-ua-mobile'));
329
+ h.set('accept', '*/*');
330
+ for (const k of ['accept-encoding', 'accept-language', 'cookie', 'priority', 'sec-fetch-dest', 'sec-fetch-mode', 'sec-fetch-site'])
331
+ h.set(k, base.get(k));
332
+ const res = await t.send({ method: 'HEAD', url: `${ORIGIN}/api/v1/video/transcode/enable/`, headers: h.pairs() });
333
+ if (res.status >= 400)
334
+ throw new SpecificAIError('UPSTREAM', `transcode HEAD 返回 HTTP ${res.status}`);
335
+ const parts = String(res.headers.get('x-ware-csrf-token') ?? '').split(',');
336
+ if (parts.length !== 5 || !parts[1])
337
+ throw new SpecificAIError('UPSTREAM', 'transcode HEAD 缺少五段 x-ware-csrf-token');
338
+ t.device.secsdk_csrf_token = parts[1];
339
+ if (!t.ttCsrfToken)
340
+ throw new SpecificAIError('UPSTREAM', 'transcode HEAD 没有写入 tt_csrf_token Cookie');
341
+ }
342
+ export async function enableVideoTranscode(t, videoId, referer = UPLOAD_REFERER) {
343
+ if (!t.ttCsrfToken || !t.secsdkCsrfToken)
344
+ await bootstrapTranscodeCsrf(t, referer);
345
+ const base = t.headers('GET', { referer, origin: ORIGIN, contentLength: '0', secFetchSite: 'same-origin' });
346
+ const h = new Headers();
347
+ h.set('sec-ch-ua-platform', base.get('sec-ch-ua-platform'));
348
+ h.set('referer', base.get('referer'));
349
+ h.set('sec-ch-ua', base.get('sec-ch-ua'));
350
+ h.set('sec-ch-ua-mobile', base.get('sec-ch-ua-mobile'));
351
+ h.set('tt-csrf-token', t.ttCsrfToken);
352
+ h.set('user-agent', base.get('user-agent'));
353
+ h.set('accept', AXIOS_ACCEPT);
354
+ h.set('x-secsdk-csrf-token', t.secsdkCsrfToken);
355
+ h.set('accept-encoding', base.get('accept-encoding'));
356
+ h.set('accept-language', base.get('accept-language'));
357
+ h.set('content-length', '0');
358
+ for (const k of ['cookie', 'origin', 'priority', 'sec-fetch-dest', 'sec-fetch-mode', 'sec-fetch-site'])
359
+ h.set(k, base.get(k));
360
+ const p = new Params([
361
+ ['video_id', videoId],
362
+ ['aid', '1988'],
363
+ ]);
364
+ const res = await t.send({ method: 'POST', url: `${ORIGIN}/api/v1/video/transcode/enable/?${p.toQuery()}`, headers: h.pairs() });
365
+ return t.json(res, { path: '/api/v1/video/transcode/enable/' });
366
+ }
367
+ export async function videoTranscodeResult(t, videoId, o) {
368
+ const body = compactJson(new Map([
369
+ ['scene', o.scene ?? 0],
370
+ [
371
+ 'video_info',
372
+ [
373
+ new Map([
374
+ ['file_key', o.fileKey],
375
+ ['video_id', videoId],
376
+ ['original_width', o.width],
377
+ ['original_height', o.height],
378
+ ['original_duration_ms', o.durationMs],
379
+ ]),
380
+ ],
381
+ ],
382
+ ]));
383
+ const len = String(Buffer.byteLength(body));
384
+ const base = t.headers('POST', { referer: o.referer ?? UPLOAD_REFERER, origin: ORIGIN, contentLength: len, secFetchSite: 'same-origin' });
385
+ const h = new Headers();
386
+ h.set('sec-ch-ua-platform', base.get('sec-ch-ua-platform'));
387
+ h.set('referer', base.get('referer'));
388
+ h.set('user-agent', base.get('user-agent'));
389
+ h.set('accept', AXIOS_ACCEPT);
390
+ h.set('sec-ch-ua', base.get('sec-ch-ua'));
391
+ h.set('content-type', 'application/json');
392
+ h.set('sec-ch-ua-mobile', base.get('sec-ch-ua-mobile'));
393
+ h.set('accept-encoding', base.get('accept-encoding'));
394
+ h.set('accept-language', base.get('accept-language'));
395
+ h.set('content-length', len);
396
+ for (const k of ['cookie', 'origin', 'priority', 'sec-fetch-dest', 'sec-fetch-mode', 'sec-fetch-site'])
397
+ h.set(k, base.get(k));
398
+ const res = await t.send({ method: 'POST', url: `${ORIGIN}/api/v1/video/transcode/result/?aid=1988`, headers: h.pairs(), body });
399
+ return t.json(res, { path: '/api/v1/video/transcode/result/' });
400
+ }
401
+ /** 轮询到 transcode_status == 3。 */
402
+ export async function waitVideoTranscode(t, videoId, o) {
403
+ const deadline = rand.now() + (o.timeout ?? 90) * 1000;
404
+ for (;;) {
405
+ const r = await videoTranscodeResult(t, videoId, o);
406
+ if (Number(r.transcode_result?.[0]?.transcode_status ?? 0) === 3)
407
+ return r;
408
+ if (rand.now() >= deadline)
409
+ throw new SpecificAIError('UPSTREAM', 'TikTok 视频转码在限定时间内没有完成');
410
+ await rand.sleep(Math.max(50, (o.interval ?? 1) * 1000));
411
+ }
412
+ }
413
+ export function mediaOpenId(t, referer = UPLOAD_REFERER) {
414
+ return t.requestJson({ method: 'GET', path: '/api/v1/media/get/openid/', params: new Params([['aid', '1988']]), referer, signed: false, accept: AXIOS_ACCEPT });
415
+ }
416
+ /** 发布前的空项目(真实 POST,空 body,不签名)。 */
417
+ export async function projectCreate(t, creation, referer = UPLOAD_REFERER) {
418
+ const h = t.headers('GET', { referer, origin: ORIGIN, contentLength: '0', secFetchSite: 'same-origin' });
419
+ h.set('accept', AXIOS_ACCEPT);
420
+ const p = new Params([
421
+ ['creation_id', creation],
422
+ ['type', '1'],
423
+ ['aid', '1988'],
424
+ ]);
425
+ const res = await t.send({ method: 'POST', url: `${ORIGIN}/api/v1/web/project/create/?${p.toQuery()}`, headers: h.pairs() });
426
+ return t.json(res, { path: '/api/v1/web/project/create/' });
427
+ }
428
+ // ================================================================ 项目 body(字段与键序照抄浏览器)
429
+ const FONTS = [
430
+ ['Noto-KR', '3379517775'],
431
+ ['Noto-JP', '3379518023'],
432
+ ['Noto-TC', '3379517439'],
433
+ ['Noto-SC', '3382263014'],
434
+ ['Noto-Arabic', '3380773560'],
435
+ ['Noto-Myanmar', '3380773555'],
436
+ ['Noto-Khmer', '3380773556'],
437
+ ['Noto-Devanagari', '3380773557'],
438
+ ['Noto-Bengali', '3380773558'],
439
+ ['Noto-Thai', '3380773559'],
440
+ ];
441
+ const M = (entries) => new Map(entries);
442
+ /** 视频项目的 body(上游 build_creator_project_body)。 */
443
+ export function buildVideoProjectBody(o) {
444
+ const missing = Object.entries({ creation_id: o.creationId, video_id: o.videoId, cover_uri: o.coverUri, play_url: o.playUrl, filename: o.filename })
445
+ .filter(([, v]) => !v)
446
+ .map(([k]) => k);
447
+ if (missing.length)
448
+ throw new SpecificAIError('ERROR', `Creator project body 缺少字段:${missing.join(', ')}`);
449
+ const { width, height, durationMs } = o;
450
+ const fps = o.fps ?? 24;
451
+ if (Math.min(width, height, durationMs, fps) <= 0)
452
+ throw new SpecificAIError('USAGE', '视频尺寸、时长和帧率必须为正数');
453
+ const now = rand.now();
454
+ const iso = new Date(now).toISOString();
455
+ const updatedMs = String(now);
456
+ const coverBlob = `blob:https://www.tiktok.com/${rand.uuid4()}`;
457
+ const frameBlob = `blob:https://www.tiktok.com/${rand.uuid4()}`;
458
+ const coverAssetId = rand.uuid4();
459
+ const videoAssetId = rand.uuid4();
460
+ const fontAssets = FONTS.map(([name, family]) => M([
461
+ ['id', rand.uuid4()],
462
+ ['name', name],
463
+ ['type', 'font'],
464
+ ['url', M([['loki', `loki://${family}`]])],
465
+ ['metaInfo', M([['family', family], ['format', ''], ['role', 'FALLBACK']])],
466
+ ]));
467
+ const coverProject = M([
468
+ [
469
+ 'project',
470
+ M([
471
+ ['id', rand.uuid4()],
472
+ ['sdkVersion', '0.6.0-alpha.4'],
473
+ ['name', 'New Project'],
474
+ ['created', iso],
475
+ ['updated', iso],
476
+ ['description', ''],
477
+ ['mediaInfo', M([['width', 1152], ['height', 648]])],
478
+ [
479
+ 'tracks',
480
+ [
481
+ M([
482
+ ['id', rand.uuid4()],
483
+ ['type', 'image'],
484
+ ['isMain', true],
485
+ [
486
+ 'clips',
487
+ [
488
+ M([
489
+ ['id', rand.uuid4()],
490
+ ['type', 'image'],
491
+ ['assetId', coverAssetId],
492
+ ['timing', M([['stepIn', 0], ['duration', 1000], ['start', 0], ['speed', 1]])],
493
+ ['effects', M([])],
494
+ [
495
+ 'props',
496
+ M([
497
+ ['visual', M([['size', [width, height]], ['layout', M([['anchor', [0, 0]], ['offset', [0, 0]], ['position', [0, 0]], ['scale', [4 / 3, 4 / 3]]])]])],
498
+ ['crop', [0, 0, width, height]],
499
+ ]),
500
+ ],
501
+ ]),
502
+ ],
503
+ ],
504
+ ]),
505
+ ],
506
+ ],
507
+ ['scripts', []],
508
+ [
509
+ 'assets',
510
+ [
511
+ M([
512
+ ['id', coverAssetId],
513
+ ['name', 'CoverImage'],
514
+ ['type', 'image'],
515
+ ['url', M([['blob', coverBlob]])],
516
+ ['metaInfo', M([['width', width], ['height', height], ['format', '']])],
517
+ ]),
518
+ ],
519
+ ],
520
+ ]),
521
+ ],
522
+ [
523
+ 'sourceProject',
524
+ M([
525
+ ['id', rand.uuid4()],
526
+ ['sdkVersion', '0.6.0-alpha.4'],
527
+ ['name', 'add title for your video'],
528
+ ['created', iso],
529
+ ['updated', updatedMs],
530
+ ['description', 'A sample project created with the builder pattern'],
531
+ ['mediaInfo', M([['width', width], ['height', height], ['fps', fps]])],
532
+ [
533
+ 'tracks',
534
+ [
535
+ M([
536
+ ['id', rand.uuid4()],
537
+ ['type', 'video'],
538
+ ['isMain', true],
539
+ [
540
+ 'clips',
541
+ [
542
+ M([
543
+ ['id', rand.uuid4()],
544
+ ['type', 'video'],
545
+ ['assetId', videoAssetId],
546
+ ['timing', M([['stepIn', 0], ['duration', durationMs], ['start', 0], ['speed', 1]])],
547
+ ['effects', M([])],
548
+ [
549
+ 'props',
550
+ M([
551
+ ['visual', M([['size', [width, height]], ['layout', M([['offset', [0, 0]], ['anchor', [0, 0]], ['position', [0, 0]]])]])],
552
+ ['crop', [0, 0, width, height]],
553
+ ]),
554
+ ],
555
+ ]),
556
+ ],
557
+ ],
558
+ ['muted', false],
559
+ ]),
560
+ ],
561
+ ],
562
+ ['scripts', []],
563
+ [
564
+ 'assets',
565
+ [
566
+ M([
567
+ ['id', videoAssetId],
568
+ ['name', o.filename],
569
+ ['type', 'video'],
570
+ ['url', M([['http', o.playUrl]])],
571
+ ['metaInfo', M([['width', width], ['height', height], ['fps', 0], ['duration', durationMs], ['format', 'video/mp4']])],
572
+ ]),
573
+ ...fontAssets,
574
+ ],
575
+ ],
576
+ ]),
577
+ ],
578
+ ['coverSourceType', 'frame'],
579
+ ['sourceProjectTime', 0],
580
+ ['sourceProjectLastSaveFrame', frameBlob],
581
+ ]);
582
+ const body = M([
583
+ ['post_common_info', M([['creation_id', o.creationId], ['enter_post_page_from', 2], ['post_type', 3]])],
584
+ [
585
+ 'feature_common_info_list',
586
+ [
587
+ M([
588
+ ['geofencing_regions', []],
589
+ ['playlist_name', ''],
590
+ ['playlist_id', ''],
591
+ ['tcm_params', '{"commerce_toggle_info":{}}'],
592
+ ['sound_exemption', 0],
593
+ ['anchors', []],
594
+ ['vedit_common_info', M([['draft', ''], ['video_id', o.videoId]])],
595
+ [
596
+ 'privacy_setting_info',
597
+ M([
598
+ ['visibility_type', o.visibilityType ?? 1],
599
+ ['allow_duet', o.allowDuet ?? 0],
600
+ ['allow_stitch', o.allowStitch ?? 0],
601
+ ['allow_comment', o.allowComment ?? 1],
602
+ ['allow_content_reuse', o.allowContentReuse ?? 1],
603
+ ['allow_ai_remix', o.allowAiRemix ?? 1],
604
+ ]),
605
+ ],
606
+ ]),
607
+ ],
608
+ ],
609
+ [
610
+ 'single_post_req_list',
611
+ [
612
+ M([
613
+ ['batch_index', 0],
614
+ ['video_id', o.videoId],
615
+ ['is_long_video', 0],
616
+ [
617
+ 'single_post_feature_info',
618
+ M([
619
+ ['text', o.text],
620
+ ['text_extra', o.textExtra ?? []],
621
+ ['markup_text', o.text],
622
+ ['music_info', M([['origin_volume', '100']])],
623
+ [
624
+ 'cover_info',
625
+ M([
626
+ ['cover_type', 1],
627
+ ['cover_uri', o.coverUri],
628
+ ['cover_width', 486],
629
+ ['cover_height', 648],
630
+ ['blob_url', coverBlob],
631
+ ['frame_duration', 0],
632
+ ['isAutoCropFirstFrame', true],
633
+ ['coverProject', coverProject],
634
+ ['isProcessingInitialCover', false],
635
+ ['crop_type', 2],
636
+ ]),
637
+ ],
638
+ ['poster_delay', 0],
639
+ ['cloud_edit_video_height', height],
640
+ ['cloud_edit_video_width', width],
641
+ ['cloud_edit_is_use_video_canvas', false],
642
+ ['has_original_audio', 1],
643
+ ['is_upload_audio_track', false],
644
+ ['video_track_time_range_list', [M([['start_time_in_ms', 0], ['end_time_in_ms', durationMs]])]],
645
+ ['mature_theme_type', 0],
646
+ ]),
647
+ ],
648
+ ]),
649
+ ],
650
+ ],
651
+ ]);
652
+ return compactJson(body);
653
+ }
654
+ /** 图文项目的 body(上游 build_creator_photo_project_body)。 */
655
+ export function buildPhotoProjectBody(o) {
656
+ if (!/^[A-Za-z0-9_-]{21}$/.test(o.creationId))
657
+ throw new SpecificAIError('ERROR', 'Photo Mode creation_id 必须是 21 位 URL-safe 字符串');
658
+ if (!o.photos.length)
659
+ throw new SpecificAIError('USAGE', '图文至少需要一张图片');
660
+ const rows = o.photos.map((p) => M([['id', p.id], ['uri', p.uri], ['width_px', p.width_px], ['height_px', p.height_px]]));
661
+ const body = M([
662
+ [
663
+ 'feature_common_info_list',
664
+ [
665
+ M([
666
+ ['aigc_info', M([['aigc_label_type', 0]])],
667
+ ['org_game_post_use_store_region', false],
668
+ [
669
+ 'privacy_setting_info',
670
+ M([
671
+ ['allow_comment', o.allowComment ?? 1],
672
+ ['allow_duet', o.allowDuet ?? 1],
673
+ ['allow_stitch', o.allowStitch ?? 1],
674
+ ['allow_content_reuse', o.allowContentReuse ?? 1],
675
+ ['allow_ai_remix', o.allowAiRemix ?? 1],
676
+ ['visibility_type', o.visibilityType ?? 1],
677
+ ]),
678
+ ],
679
+ ['draft_id', ''],
680
+ ['tcm_params', '{"commerce_toggle_info":{}}'],
681
+ ['anchors', []],
682
+ ]),
683
+ ],
684
+ ],
685
+ ['post_common_info', M([['creation_id', o.creationId], ['enter_post_page_from', 1], ['post_type', 4]])],
686
+ [
687
+ 'single_post_req_list',
688
+ [
689
+ M([
690
+ ['batch_index', 0],
691
+ ['aweme_type', 150],
692
+ ['image_post_content', M([['title', o.title ?? ''], ['images', rows], ['cover', new Map(rows[0])]])],
693
+ ['single_post_feature_info', M([['text', o.text], ['text_extra', o.textExtra ?? []], ['markup_text', o.text]])],
694
+ ]),
695
+ ],
696
+ ],
697
+ ]);
698
+ return compactJson(body);
699
+ }
700
+ /** project/post:ticket-guard 现签,X-Bogus / X-Gnarly 用 client-data 里的时间戳(上游 post_project)。 */
701
+ export async function postProject(t, body, referer = `${ORIGIN}/tiktokstudio/upload?from=webapp&lang=zh-Hans`) {
702
+ const guard = t.ticketGuard(PROJECT_POST_PATH);
703
+ const params = new Params([
704
+ ['app_name', 'tiktok_web'],
705
+ ['channel', 'tiktok_web'],
706
+ ['device_platform', 'web'],
707
+ ['tz_name', 'Asia/Shanghai'],
708
+ ['aid', '1988'],
709
+ ]);
710
+ const h = t.headers('POST', { referer, origin: ORIGIN, contentLength: String(Buffer.byteLength(body)), secFetchSite: 'same-origin' });
711
+ h.set('accept', AXIOS_ACCEPT);
712
+ h.update(guard);
713
+ h.reorder([
714
+ 'tt-ticket-guard-client-data', 'sec-ch-ua-platform', 'referer', 'sec-ch-ua', 'sec-ch-ua-mobile', 'tt-ticket-guard-web-version', 'tt-ticket-guard-version',
715
+ 'user-agent', 'accept', 'tt-ticket-guard-public-key', 'content-type', 'tt-ticket-guard-iteration-version', 'accept-encoding', 'accept-language',
716
+ 'content-length', 'cookie', 'origin', 'priority', 'sec-fetch-dest', 'sec-fetch-mode', 'sec-fetch-site',
717
+ ], true);
718
+ const { signRequest } = await import('./sign.js');
719
+ const unsigned = `${params.toQuery()}&msToken=${t.msToken}`;
720
+ params.update(signRequest({
721
+ url: `${ORIGIN}${PROJECT_POST_PATH}?${unsigned}`,
722
+ method: 'POST',
723
+ body,
724
+ userAgent: t.ua,
725
+ referer,
726
+ metrics: t.metrics,
727
+ signingTimestamp: ticketTimestamp(guard['tt-ticket-guard-client-data']),
728
+ }));
729
+ const res = await t.send({ method: 'POST', url: `${ORIGIN}${PROJECT_POST_PATH}?${params.toQuery()}`, headers: h.pairs(), body });
730
+ const result = await t.json(res, { path: PROJECT_POST_PATH });
731
+ for (const item of result.single_post_resp_list ?? []) {
732
+ if (item?.status_code != null && String(item.status_code) !== '0') {
733
+ throw new SpecificAIError('UPSTREAM', `发布失败:status_code=${item.status_code}`, { detail: { code: item.status_code, message: item.status_msg } });
734
+ }
735
+ }
736
+ return result;
737
+ }
738
+ /** 读 MP4 / MOV 的 moov:视频轨的尺寸(tkhd)、时长(mdhd)、帧率(stts 的样本数 / 时长)。 */
739
+ export function mp4Meta(data) {
740
+ const t = mp4VideoTrack(data);
741
+ const seconds = t.timescale ? t.duration / t.timescale : 0;
742
+ const fps = t.sttsSamples && seconds ? Math.max(1, Math.round(t.sttsSamples / seconds)) : 24;
743
+ if (!(t.displayWidth > 0 && t.displayHeight > 0 && seconds > 0))
744
+ throw new SpecificAIError('USAGE', '视频里没有可识别的视频轨(尺寸 / 时长)');
745
+ return { width: t.displayWidth, height: t.displayHeight, durationMs: Math.max(1, Math.round(seconds * 1000)), fps };
746
+ }
747
+ /** 只含一个 deflate 文件的 ZIP(浏览器上传给 tiktok-ai-frame 的 0.jpeg 压缩包)。 */
748
+ export function zipOne(name, data) {
749
+ const nameBuf = Buffer.from(name, 'utf8');
750
+ const packed = deflateRawSync(data);
751
+ const crc = crc32(data) >>> 0;
752
+ const d = new Date(rand.now());
753
+ const dosTime = (d.getHours() << 11) | (d.getMinutes() << 5) | (d.getSeconds() >> 1);
754
+ const dosDate = ((d.getFullYear() - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate();
755
+ const local = Buffer.alloc(30);
756
+ local.writeUInt32LE(0x04034b50, 0);
757
+ local.writeUInt16LE(20, 4);
758
+ local.writeUInt16LE(0, 6);
759
+ local.writeUInt16LE(8, 8);
760
+ local.writeUInt16LE(dosTime, 10);
761
+ local.writeUInt16LE(dosDate, 12);
762
+ local.writeUInt32LE(crc, 14);
763
+ local.writeUInt32LE(packed.length, 18);
764
+ local.writeUInt32LE(data.length, 22);
765
+ local.writeUInt16LE(nameBuf.length, 26);
766
+ const central = Buffer.alloc(46);
767
+ central.writeUInt32LE(0x02014b50, 0);
768
+ central.writeUInt16LE(20, 4);
769
+ central.writeUInt16LE(20, 6);
770
+ central.writeUInt16LE(0, 8);
771
+ central.writeUInt16LE(8, 10);
772
+ central.writeUInt16LE(dosTime, 12);
773
+ central.writeUInt16LE(dosDate, 14);
774
+ central.writeUInt32LE(crc, 16);
775
+ central.writeUInt32LE(packed.length, 20);
776
+ central.writeUInt32LE(data.length, 24);
777
+ central.writeUInt16LE(nameBuf.length, 28);
778
+ central.writeUInt32LE(0, 42);
779
+ const localSize = local.length + nameBuf.length + packed.length;
780
+ const end = Buffer.alloc(22);
781
+ end.writeUInt32LE(0x06054b50, 0);
782
+ end.writeUInt16LE(1, 8);
783
+ end.writeUInt16LE(1, 10);
784
+ end.writeUInt32LE(central.length + nameBuf.length, 12);
785
+ end.writeUInt32LE(localSize, 16);
786
+ return Uint8Array.from(Buffer.concat([local, nameBuf, packed, central, nameBuf, end]));
787
+ }
788
+ /** 解图片:尺寸、PNG(视频封面)、JPEG q92(ai-frame)。 */
789
+ export async function picture(data) {
790
+ const { createCanvas, loadImage } = await import('@napi-rs/canvas');
791
+ let img;
792
+ try {
793
+ img = await loadImage(Buffer.from(data));
794
+ }
795
+ catch {
796
+ throw new SpecificAIError('USAGE', '无法解析图片');
797
+ }
798
+ const canvas = createCanvas(img.width, img.height);
799
+ const ctx = canvas.getContext('2d');
800
+ ctx.fillStyle = '#ffffff';
801
+ ctx.fillRect(0, 0, img.width, img.height);
802
+ ctx.drawImage(img, 0, 0);
803
+ return { width: img.width, height: img.height, png: await canvas.encode('png'), jpeg: await canvas.encode('jpeg', 92) };
804
+ }
805
+ export { randbelow };