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,609 @@
1
+ import { CookieJar } from '../../../core/cookies.js';
2
+ import { SpecificAIError } from '../../../core/errors.js';
3
+ import { imageSize } from '../../../core/image.js';
4
+ import { mp4AvgFrameRate, mp4VideoTrack } from '../../../core/mp4.js';
5
+ import { jsonLoads, pyFloatStr, pyRound } from '../../../core/py.js';
6
+ import * as rand from '../../../core/rand.js';
7
+ import { authError, scope } from '../../../core/toolkit.js';
8
+ import { getdss, isAuthFailure, jsonOf } from './client.js';
9
+ import { cspl, Creator, PyFloat } from './creator.js';
10
+ import { generateA1, generateWebId } from './login.js';
11
+ import { creatorProfileData, creatorRapFingerprint, generateWebsectiga, rapParam, uploadSignature, urlSign } from './js.js';
12
+ import { AS, COOKIE_DOMAIN, CREATOR, CREATOR_ORDER, CREATOR_REFERENCE, CUSTOMER, EDITH, LOGIN_LANG, navigationHeaders, orderedHeaders, ROS_UPLOAD, UA, WEB } from './profile.js';
13
+ /**
14
+ * 上游 apis/xhs_creator_apis.py(XHS_Creator_Apis)与 apis/xhs_creator_login_apis.py(XHSCreatorLoginApi)。
15
+ */
16
+ const NOTE_MANAGER = `${CREATOR}/new/note-manager`;
17
+ const PUBLISH_REFERER = `${CREATOR}/publish/publish?source=official&from=tab_switch`;
18
+ const SECURITY_LENGTHS = { websectiga: 64, sec_poison_id: 36, gid: 72 };
19
+ // ================================================================ 业务接口
20
+ /** get_user_info(笔记管理页的 user/info,带 cache-control / pragma)。 */
21
+ export function userInfo(c) {
22
+ return c.request('/api/galaxy/user/info', '', 'GET', {
23
+ referer: NOTE_MANAGER,
24
+ site: 'same-origin',
25
+ b1Profile: 'login',
26
+ mnsProfile: 'publish_user_info',
27
+ extra: { 'cache-control': 'no-cache', pragma: 'no-cache' },
28
+ order: CREATOR_ORDER.noteManagerUserInfo,
29
+ });
30
+ }
31
+ /** get_posted_notes_page:page 从 0 开始,响应 data.page 是下一页游标,-1 表示结束。 */
32
+ export function postedNotes(c, page = 0, tab = 0, steady = false) {
33
+ return c.request(cspl('/api/galaxy/v2/creator/note/user/posted', { tab: String(tab), page: String(page) }), '', 'GET', {
34
+ referer: NOTE_MANAGER,
35
+ site: 'same-origin',
36
+ b1Profile: 'note_manager',
37
+ mnsProfile: steady ? 'note_manager_steady' : 'note_manager',
38
+ order: CREATOR_ORDER.noteManager,
39
+ storeOrder: true,
40
+ });
41
+ }
42
+ /** get_topic:话题搜索(edith 域)。 */
43
+ export function searchTopic(c, keyword) {
44
+ return c.request('/web_api/sns/v1/search/topic', { keyword, suggest_topic_request: { title: '', desc: `#${keyword}` }, page: { page_size: 20, page: 1 } }, 'POST', { target: EDITH });
45
+ }
46
+ /** get_location_info:地点搜索(固定上海坐标,上游 get_loc_data)。 */
47
+ export function searchPoi(c, keyword) {
48
+ return c.request('/web_api/sns/v1/local/poi/creator/search', { latitude: 31.161327166987615, longitude: 121.45301809352632, keyword, page: 1, size: 50, source: 'WEB', type: 3 }, 'POST', { target: EDITH });
49
+ }
50
+ /** get_fileIds:上传许可;406 / code=-1 时换新签名重发(最多 3 次)。 */
51
+ export async function uploadPermit(c, mediaType) {
52
+ const api = cspl('/api/media/v1/upload/creator/permit', { biz_name: 'spectrum', scene: mediaType, file_count: '1', version: '1', source: 'web' });
53
+ let last = null;
54
+ for (let attempt = 0; attempt < 3; attempt++) {
55
+ const r = await c.business(api, '', 'GET', {
56
+ referer: PUBLISH_REFERER,
57
+ site: 'same-origin',
58
+ mnsProfile: mediaType === 'video' ? 'publish_permit_video' : 'publish_permit_image',
59
+ b1Profile: 'login',
60
+ });
61
+ const xt = r.headers.find(([k]) => k === 'x-t')?.[1] ?? '';
62
+ const res = await c.send({ url: r.url, headers: r.headers });
63
+ const body = await res.json().catch(() => ({}));
64
+ last = body;
65
+ const permits = body?.data?.uploadTempPermits ?? [];
66
+ if ((body?.success || body?.data?.result?.success) && permits.length)
67
+ return { permit: permits[0], xt };
68
+ const code = body?.code ?? body?.data?.result?.code;
69
+ if ((res.status === 406 || code === -1) && attempt < 2) {
70
+ c.ctx.log.warn(`上传许可被拒绝(HTTP ${res.status}, code=${code}),重发(${attempt + 1}/3)`);
71
+ continue;
72
+ }
73
+ break;
74
+ }
75
+ if (isAuthFailure(last))
76
+ throw authError(c.ctx, String(last?.msg ?? last?.message ?? '') || undefined);
77
+ throw new SpecificAIError('UPSTREAM', String(last?.msg ?? last?.message ?? '获取上传许可失败'), { detail: { code: last?.code } });
78
+ }
79
+ /** upload_media:申请许可 → q-signature → PUT 到 ROS。返回 fileId(及视频的 video_id)和上传到的绝对地址。 */
80
+ export async function uploadMedia(c, data, mediaType) {
81
+ const { permit, xt } = await uploadPermit(c, mediaType);
82
+ const host = String(permit.uploadAddr || new URL(ROS_UPLOAD).host);
83
+ const url = host.startsWith('http') ? host : `https://${host}`;
84
+ const fileId = String(permit.fileIds[0]).split('/').at(-1);
85
+ const message = `${xt.slice(0, 10)};${String(permit.expireTime).slice(0, 10)}`;
86
+ const signature = uploadSignature(message, fileId, data.length, host);
87
+ const headers = [
88
+ ['accept', '*/*'],
89
+ ['accept-encoding', 'gzip, deflate, br, zstd'],
90
+ ['accept-language', 'zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7,ja;q=0.6'],
91
+ ['authorization', `q-sign-algorithm=sha1&q-ak=null&q-sign-time=${message}&q-key-time=${message}&q-header-list=content-length;host&q-url-param-list=&q-signature=${signature}`],
92
+ ['cache-control', ''],
93
+ ['content-type', ''],
94
+ ['origin', CREATOR],
95
+ ['referer', `${CREATOR}/`],
96
+ ['sec-fetch-dest', 'empty'],
97
+ ['sec-fetch-mode', 'cors'],
98
+ ['sec-fetch-site', host.includes('xhscdn.com') ? 'cross-site' : 'same-site'],
99
+ ['user-agent', c.state.release.userAgent],
100
+ ['x-cos-security-token', String(permit.token)],
101
+ ];
102
+ // ROS 上传域收不到 creator 的登录 cookie
103
+ const target = `${url}/spectrum/${fileId}`;
104
+ const res = await c.send({ method: 'PUT', url: target, headers, body: data });
105
+ if (!res.ok)
106
+ throw new SpecificAIError('UPSTREAM', `上传失败:HTTP ${res.status}`, { detail: { status: res.status } });
107
+ const videoId = mediaType === 'video' ? res.headers.get('X-Ros-Video-Id') : null;
108
+ if (mediaType === 'video' && !videoId)
109
+ throw new SpecificAIError('UPSTREAM', '上传响应缺少 X-Ros-Video-Id');
110
+ return { fileId, videoId, url: target };
111
+ }
112
+ /** query_transcode(edith 域,沿用上传许可的签名档位)。 */
113
+ export function queryTranscode(c, videoId) {
114
+ return c.request(cspl('/web_api/sns/capa/postgw/query_transcode', { video_id: videoId, need_transcode: 'false', resource_type: '0' }), '', 'GET', {
115
+ target: EDITH,
116
+ b1Profile: 'login',
117
+ mnsProfile: 'publish_permit',
118
+ });
119
+ }
120
+ /** upload_media(image):上传图片,宽高按上游 get_file_info(宽大于两倍高时,高按宽的一半报)。 */
121
+ export async function uploadImage(c, data, name = '图片') {
122
+ const size = imageSize(data);
123
+ if (!size)
124
+ throw new SpecificAIError('USAGE', `无法识别的图片:${name}`);
125
+ const up = await uploadMedia(c, data, 'image');
126
+ const height = size.width > 2 * size.height ? Math.floor(size.width / 2) : size.height;
127
+ return { fileId: up.fileId, url: up.url, width: size.width, height, size: data.length, mimeType: 'image/png' };
128
+ }
129
+ /**
130
+ * extract_video_cover_and_metadata 的元数据部分。上游用 opencv 打开视频:帧率是 ffmpeg 的 avg_frame_rate,
131
+ * 帧数是 stsz 的样本数,宽高是编码尺寸(旋转 90° / 270° 时对调),时长 = int(帧数 / 帧率 × 1000)。
132
+ * 这里读 MP4 的盒子得到同样的值;读不出来(不是 MP4 / MOV)时返回 null。
133
+ */
134
+ export function videoMetadata(video) {
135
+ let t;
136
+ try {
137
+ t = mp4VideoTrack(video);
138
+ }
139
+ catch {
140
+ return null;
141
+ }
142
+ const fps = mp4AvgFrameRate(t);
143
+ let width = t.codedWidth || t.displayWidth;
144
+ let height = t.codedHeight || t.displayHeight;
145
+ if (t.rotation % 180 !== 0)
146
+ [width, height] = [height, width];
147
+ const duration = fps ? Math.trunc((t.sampleCount / fps) * 1000) : 0;
148
+ return {
149
+ video: {
150
+ bitrate: null,
151
+ colour_primaries: 'BT.709',
152
+ duration,
153
+ format: 'AVC',
154
+ frame_rate: fps ? new PyFloat(pyRound(fps, 3)) : 0,
155
+ height,
156
+ matrix_coefficients: 'BT.709',
157
+ rotation: 0,
158
+ transfer_characteristics: 'BT.709',
159
+ width,
160
+ },
161
+ audio: { bitrate: null, channels: 2, duration, format: 'AAC', sampling_rate: 48000 },
162
+ };
163
+ }
164
+ /** 读不出元数据时的占位(时长、帧率为 0,宽高取封面):平台转码后自己补全。 */
165
+ function emptyVideoMeta(width, height) {
166
+ return {
167
+ video: { bitrate: null, colour_primaries: 'BT.709', duration: 0, format: 'AVC', frame_rate: 0, height, matrix_coefficients: 'BT.709', rotation: 0, transfer_characteristics: 'BT.709', width },
168
+ audio: { bitrate: null, channels: 2, duration: 0, format: 'AAC', sampling_rate: 48000 },
169
+ };
170
+ }
171
+ /**
172
+ * post_note 的视频部分:元数据 → 上传视频 → 上传封面 → 轮询转码 → get_post_note_video_data。
173
+ * 上游用 opencv 截首帧当封面;specific-ai 不带视频解码器,封面由调用方给。
174
+ */
175
+ export async function videoNoteData(c, note, video, cover) {
176
+ const meta = videoMetadata(video);
177
+ if (!meta)
178
+ c.ctx.log.warn('读不出视频的时长、宽高、帧率(只支持 MP4 / MOV),按 0 上报,由平台转码后补全');
179
+ const up = await uploadMedia(c, video, 'video');
180
+ const coverInfo = await uploadImage(c, cover, '封面');
181
+ for (let i = 0; i < 20; i++) {
182
+ const r = await queryTranscode(c, up.videoId);
183
+ const d = r?.data ?? {};
184
+ if (!r?.success || d.hasFirstFrame === true || d.has_first_frame === true || d.firstFrameFileId || d.first_frame_file_id || [2, 'success', 'SUCCESS'].includes(d.status) || !Object.keys(d).length)
185
+ break;
186
+ if (i === 19)
187
+ throw new SpecificAIError('UPSTREAM', '视频转码超时:轮询 20 次仍未完成');
188
+ await rand.sleep(3000, c.ctx.signal);
189
+ }
190
+ return videoData(note, up.fileId, coverInfo, meta ?? emptyVideoMeta(coverInfo.width, coverInfo.height));
191
+ }
192
+ /** encryption:图片文件加密(www 域)。 */
193
+ export function fileEncryption(c, fileId) {
194
+ return c.request(cspl('/web_api/sns/v5/creator/file/encryption', { file_id: fileId, type: 'image', ts: String(rand.now()), sign: urlSign(fileId) }), '', 'GET', { target: WEB });
195
+ }
196
+ const BUSINESS_BINDS = '{"version":1,"noteId":0,"bizType":0,"noteOrderBind":{},"notePostTiming":{},"noteCollectionBind":{"id":""},"noteSketchCollectionBind":{"id":""},"coProduceBind":{"enable":true},"noteCopyBind":{"copyable":true},"interactionPermissionBind":{"commentPermission":0},"optionRelationList":[]}';
197
+ const scheduledBinds = (postTime) => `{"version":1,"noteId":0,"bizType":13,"noteOrderBind":{},"notePostTiming":{"postTime":"${postTime}"},"noteCollectionBind":{"id":""}}`;
198
+ const SOURCE = '{"type":"web","ids":"","extraInfo":"{\\"subType\\":\\"official\\",\\"systemId\\":\\"web\\"}"}';
199
+ const CONTEXT_JSON = '{"recommend_title":{"recommend_title_id":"","is_use":3,"used_index":-1},"recommendTitle":[],"recommend_topics":{"used":[]}}';
200
+ function common(type, n, withContext) {
201
+ const out = {
202
+ type,
203
+ title: n.title,
204
+ note_id: '',
205
+ desc: n.desc,
206
+ source: SOURCE,
207
+ business_binds: n.postTime == null ? BUSINESS_BINDS : scheduledBinds(n.postTime),
208
+ ats: [],
209
+ hash_tag: [],
210
+ post_loc: n.postLoc ?? {},
211
+ privacy_info: { op_type: 1, type: n.privacy, user_ids: [] },
212
+ goods_info: {},
213
+ biz_relations: [],
214
+ capa_trace_info: { contextJson: withContext },
215
+ };
216
+ // 浏览器实抓:未选地点时 common 里不带 post_loc
217
+ if (!n.postLoc)
218
+ delete out.post_loc;
219
+ return out;
220
+ }
221
+ /** get_post_note_image_data。 */
222
+ export function imageNoteData(n, images) {
223
+ return {
224
+ common: common('normal', n, CONTEXT_JSON),
225
+ image_info: {
226
+ images: images.map((i) => ({
227
+ file_id: `spectrum/${i.fileId}`,
228
+ width: i.width,
229
+ height: i.height,
230
+ metadata: { source: -1 },
231
+ stickers: { version: 2, floating: [] },
232
+ extra_info_json: `{"mimeType":${JSON.stringify(i.mimeType)},"image_metadata":{"bg_color":"","origin_size":${pyFloatStr(i.size / 1024)}}}`,
233
+ })),
234
+ },
235
+ video_info: null,
236
+ };
237
+ }
238
+ /** get_post_note_video_data。 */
239
+ export function videoData(n, videoFileId, cover, meta) {
240
+ const duration = Number(meta.video.duration ?? 0);
241
+ const vid = `spectrum/${videoFileId}`;
242
+ const cid = `spectrum/${cover.fileId}`;
243
+ return {
244
+ common: common('video', n, CONTEXT_JSON),
245
+ image_info: null,
246
+ video_info: {
247
+ fileid: vid,
248
+ file_id: vid,
249
+ format_width: meta.video.width || 0,
250
+ format_height: meta.video.height || 0,
251
+ video_preview_type: '',
252
+ composite_metadata: { video: meta.video, audio: meta.audio },
253
+ timelines: [],
254
+ cover: {
255
+ fileid: cid,
256
+ file_id: cid,
257
+ height: cover.height || meta.video.height || 0,
258
+ width: cover.width || meta.video.width || 0,
259
+ frame: { ts: 0, is_user_select: false, is_upload: false },
260
+ stickers: { version: 2, neptune: [] },
261
+ fonts: [],
262
+ extra_info_json: '{}',
263
+ },
264
+ chapters: [],
265
+ chapter_sync_text: false,
266
+ segments: {
267
+ count: 1,
268
+ need_slice: false,
269
+ items: [{ mute: 0, speed: 1, start: 0, duration: new PyFloat(pyRound(duration / 1000, 3)), transcoded: 0, media_source: 1, original_metadata: { video: meta.video, audio: meta.audio } }],
270
+ },
271
+ entrance: 'web',
272
+ },
273
+ };
274
+ }
275
+ /** post_note 的最后一步:签名(referer 是站点根)+ Creator rap 指纹 + 边缘拒绝(code=-1)重试。 */
276
+ export async function postNote(c, data) {
277
+ const api = '/web_api/sns/v2/note';
278
+ const r = await c.business(api, data, 'POST', { referer: `${CREATOR}/`, target: EDITH, order: null });
279
+ const rap = rapParam(api, r.body, creatorRapFingerprint());
280
+ const headers = r.headers.filter(([k]) => k !== 'cookie');
281
+ // 上游不排序:模板顺序 + x-rap-param,curl_cffi 再追加 cookie 与 accept-encoding
282
+ headers.push(['x-rap-param', rap], ...r.headers.filter(([k]) => k === 'cookie'), ['accept-encoding', 'gzip, deflate, br, zstd']);
283
+ let body;
284
+ for (let attempt = 0; attempt < 3; attempt++) {
285
+ body = await c.json({ method: 'POST', url: r.url, headers, body: r.body });
286
+ if (body?.success || body?.code !== -1)
287
+ break;
288
+ c.ctx.log.warn(`发布被边缘拒绝(code=-1),重发(${attempt + 1}/3)`);
289
+ }
290
+ return body;
291
+ }
292
+ /**
293
+ * 宽松解析:不看状态码,不是 JSON 时为 {}。只用在上游本身就容错的两处:ds 程序取不到时退回 ds 接口
294
+ * (_bootstrap_dsl_program),会话探测失败按「没有已有会话」处理。其余登录请求一律用 jsonOf(状态码判风控)。
295
+ */
296
+ async function lenientJson(res) {
297
+ try {
298
+ return jsonLoads(await res.text());
299
+ }
300
+ catch {
301
+ return {};
302
+ }
303
+ }
304
+ /**
305
+ * customer 域 qr-code / verify-code 的 HTTP 406 是按设备会话打标的概率闸门(上游 LOGIN_SESSION_MAX_ATTEMPTS 的注释):
306
+ * 不算风控,由调用方整包重建匿名设备再试。
307
+ */
308
+ const GATED = 406;
309
+ /** 上游 _cookies_for_url:customer 域且安全 cookie 就绪时把 loadts 挪到最后。 */
310
+ function loginCookies(c, url) {
311
+ const cookies = c.wire(url);
312
+ if (new URL(url).host === new URL(CUSTOMER).host && 'gid' in cookies && 'websectiga' in cookies && 'loadts' in cookies) {
313
+ const loadts = cookies.loadts;
314
+ delete cookies.loadts;
315
+ cookies.loadts = loadts;
316
+ }
317
+ return cookies;
318
+ }
319
+ async function signed(c, api, data, method, o) {
320
+ const s = await c.sign(api, data, method, { origin: CREATOR, referer: `${CREATOR}/`, site: 'same-site', trace: false, clientHints: false, ...o }, false);
321
+ s.headers['accept-language'] = LOGIN_LANG;
322
+ return s;
323
+ }
324
+ async function post(c, url, headers, kind, body, retry = false) {
325
+ const pairs = orderedHeaders(headers, CREATOR_ORDER[kind], loginCookies(c, url));
326
+ for (let attempt = 1;; attempt++) {
327
+ const res = await c.send({ method: 'POST', url, headers: pairs, body });
328
+ if (!retry || res.status !== 406 || attempt >= 5)
329
+ return res;
330
+ }
331
+ }
332
+ async function get(c, url, headers, kind, retry = false) {
333
+ const pairs = orderedHeaders(headers, CREATOR_ORDER[kind], loginCookies(c, url));
334
+ for (let attempt = 1;; attempt++) {
335
+ const res = await c.send({ url, headers: pairs });
336
+ if (!retry || res.status !== 406 || attempt >= 5)
337
+ return res;
338
+ }
339
+ }
340
+ const loadtsUndefined = (c) => `${c.state.loadts};undefined`;
341
+ export class CreatorLogin {
342
+ c;
343
+ pendingDsl = '';
344
+ pendingProgram = '';
345
+ constructor(c) {
346
+ this.c = c;
347
+ }
348
+ /** 上游 generate_init_cookies(complete_security=False):登录页导航 + 匿名 cookie + honeypot。 */
349
+ async initCookies() {
350
+ const c = this.c;
351
+ c.jar.cookies.splice(0);
352
+ c.resetState();
353
+ await c.send({ url: `${CREATOR}/login`, headers: orderedHeaders(navigationHeaders(), CREATOR_ORDER.navigation, null, { optional: ['cookie'] }), redirect: 'manual' });
354
+ const ts = rand.now();
355
+ const a1 = generateA1();
356
+ const webId = generateWebId(a1);
357
+ for (const [k, v] of Object.entries({ ets: String(ts), webBuild: CREATOR_REFERENCE.release.webBuild, xsecappid: 'ugc', loadts: String(ts + rand.randint(50, 200)), a1, webId }))
358
+ c.setCookie(k, v);
359
+ c.sync();
360
+ await this.honeypot();
361
+ }
362
+ /** 上游 _fetch_honeypot:launcher 的 honeypot 请求,不签名,响应不用。 */
363
+ async honeypot() {
364
+ const h = { 'user-agent': UA, accept: 'application/json, text/plain, */*', 'accept-language': LOGIN_LANG, 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'same-site', referer: `${CREATOR}/`, priority: 'u=1, i', origin: CREATOR, 'content-type': 'application/json' };
365
+ await post(this.c, `${AS}/api/p/pj`, h, 'honeypot', '{"callFrom":"ugc"}');
366
+ }
367
+ /**
368
+ * 上游 bootstrap_publish_navigation:带着主站会话加载一次发布页。响应会换出创作者中心自己的 HttpOnly 会话 cookie
369
+ * (customer-sso-sid、access-token-creator 等),没有这一步,之后的上传许可接口会被边缘网关拒绝(406)。
370
+ */
371
+ async publishNavigation(path = '/publish/publish?source=official') {
372
+ const c = this.c;
373
+ const res = await c.send({ url: CREATOR + path, headers: orderedHeaders(navigationHeaders('same-site'), CREATOR_ORDER.navigation, loginCookies(c, CREATOR), { optional: ['cookie'] }) });
374
+ if (res.status >= 400)
375
+ throw new SpecificAIError('UPSTREAM', `创作者中心发布页加载失败(HTTP ${res.status})`, { detail: { status: res.status } });
376
+ }
377
+ /**
378
+ * 上游 _finish_security_bootstrap:redcaptcha → ds 程序 → sbtsource(都是 0201/nop)。
379
+ * activate 为 false 时先不安装 DS:主站会话桥接过来的,还要在 0201 上发一次 user/info。
380
+ */
381
+ async bootstrap(activate = true) {
382
+ const c = this.c;
383
+ const rc = await signed(c, '/api/redcaptcha/v2/getconfig', {}, 'POST', { trace: true, authorization: false, tier: '0201', b1Profile: 'login', dslPairValue: loadtsUndefined(c) });
384
+ await post(c, `${EDITH}/api/redcaptcha/v2/getconfig`, rc.headers, 'redcaptcha', rc.body, true);
385
+ const ds = await signed(c, '/api/sec/v1/scripting', { callFrom: 'creator-platform', callback: '', type: 'ds', appId: 'ugc' }, 'POST', { tier: '0201', b1Profile: 'login', dslPairValue: loadtsUndefined(c) });
386
+ ds.headers['content-type'] = 'application/json';
387
+ const body = await lenientJson(await post(c, `${AS}/api/sec/v1/scripting`, ds.headers, 'security', ds.body));
388
+ let code = String(body?.data?.data ?? '');
389
+ let dsl = getdss(code) ?? '';
390
+ if (!dsl || !code) {
391
+ const b = await c.dsBundle();
392
+ dsl ||= b.dsl;
393
+ code ||= b.program;
394
+ }
395
+ this.pendingDsl = dsl;
396
+ this.pendingProgram = code;
397
+ const sbt = await signed(c, '/api/sec/v1/sbtsource', { callFrom: 'creator-platform', appId: 'ugc' }, 'POST', { tier: '0201', b1Profile: 'login', dslPairValue: loadtsUndefined(c) });
398
+ await post(c, `${AS}/api/sec/v1/sbtsource`, sbt.headers, 'security', sbt.body, true);
399
+ if (activate)
400
+ await this.activate();
401
+ }
402
+ /** 上游 _activate_security:安装服务端 DS 程序,之后的请求进入 0101/a1。已装好时跳过。 */
403
+ async activate() {
404
+ const c = this.c;
405
+ if (c.state.securityReady && c.state.dsl && c.state.dsProgram)
406
+ return;
407
+ if (!this.pendingDsl || !this.pendingProgram) {
408
+ const b = await c.dsBundle();
409
+ this.pendingDsl ||= b.dsl;
410
+ this.pendingProgram ||= b.program;
411
+ }
412
+ c.state.activateSecurity(this.pendingDsl, this.pendingProgram);
413
+ }
414
+ /** 上游 _prepare_login_session 的后半:zones + service-ticket type=tgt 探测已有会话。 */
415
+ async probeSession() {
416
+ const c = this.c;
417
+ const zonesApi = cspl('/api/cas/customer/web/zones', { service: CREATOR });
418
+ const z = await signed(c, zonesApi, '', 'GET', { includeOrigin: true, tier: '0101', mnsProfile: 'login_early', b1Profile: 'login' });
419
+ z.headers['x-ratelimit-meta'] = `host=${new URL(CREATOR).host}`;
420
+ const zonesUrl = CUSTOMER + zonesApi;
421
+ const zonesPairs = orderedHeaders(z.headers, CREATOR_ORDER.casGet, loginCookies(c, zonesUrl));
422
+ const t = await signed(c, '/api/cas/customer/web/service-ticket', { service: CREATOR, source: '', type: 'tgt' }, 'POST', { tier: '0101', mnsProfile: 'login_early', b1Profile: 'login' });
423
+ t.headers['x-ratelimit-meta'] = `host=${new URL(CREATOR).host}`;
424
+ const ticketUrl = `${CUSTOMER}/api/cas/customer/web/service-ticket`;
425
+ const ticketPairs = orderedHeaders(t.headers, CREATOR_ORDER.casPost, loginCookies(c, ticketUrl));
426
+ try {
427
+ for (let i = 1; i <= 5; i++)
428
+ if ((await c.send({ url: zonesUrl, headers: zonesPairs })).status !== 406)
429
+ break;
430
+ }
431
+ catch (err) {
432
+ c.ctx.log.debug(`区号列表加载失败:${err.message}`);
433
+ }
434
+ let res;
435
+ for (let i = 1; i <= 5; i++)
436
+ if ((res = await c.send({ method: 'POST', url: ticketUrl, headers: ticketPairs, body: t.body })).status !== 406)
437
+ break;
438
+ const d = (await lenientJson(res))?.data ?? {};
439
+ return { active: Boolean(d.ticket || d.type === 'at') };
440
+ }
441
+ /** 上游 _complete_security:seccallback → websectiga,webprofile → gid。 */
442
+ async completeSecurity() {
443
+ const c = this.c;
444
+ const s = await signed(c, '/api/sec/v1/scripting', { callFrom: 'creator-platform', callback: 'seccallback' }, 'POST', { tier: '0101', mnsProfile: 'login_callback', b1Profile: 'login' });
445
+ s.headers['content-type'] = 'application/json';
446
+ const body = await jsonOf(await post(c, `${AS}/api/sec/v1/scripting`, s.headers, 'security', s.body));
447
+ const poison = body?.data?.secPoisonId ?? body?.data?.sec_poison_id;
448
+ const code = String(body?.data?.data ?? '');
449
+ if (!poison || !code)
450
+ throw new SpecificAIError('UPSTREAM', 'Creator seccallback 响应缺少 sec_poison_id 或安全程序');
451
+ const tiga = await generateWebsectiga(code, { userAgent: UA, platform: 'Win32', pageUrl: `${CREATOR}/login` });
452
+ c.setCookie('websectiga', tiga);
453
+ c.setCookie('sec_poison_id', String(poison));
454
+ c.sync();
455
+ this.require(['websectiga', 'sec_poison_id']);
456
+ if ((c.shared().gid ?? '').length !== SECURITY_LENGTHS.gid) {
457
+ const data = { platform: 'Windows', sdkVersion: CREATOR_REFERENCE.release.webProfileSdkVersion, svn: '2', profileData: creatorProfileData(c.state.profileDataOptions(`${CREATOR}/login`, `${CREATOR}/login`)) };
458
+ const w = await signed(c, '/api/sec/v1/shield/webprofile', data, 'POST', { tier: '0101', mnsProfile: 'login_ready', b1Profile: 'login' });
459
+ w.headers['content-type'] = 'application/json';
460
+ await post(c, `${AS}/api/sec/v1/shield/webprofile`, w.headers, 'security', w.body);
461
+ if (c.shared().gid)
462
+ c.state.p1 += 1;
463
+ }
464
+ this.require();
465
+ }
466
+ require(names = Object.keys(SECURITY_LENGTHS)) {
467
+ const cur = this.c.shared();
468
+ const bad = names.filter((n) => (cur[n] ?? '').length !== SECURITY_LENGTHS[n]);
469
+ if (bad.length)
470
+ throw new SpecificAIError('RISK_CONTROL', `创作者中心安全初始化不完整:${bad.join(', ')}`, { detail: { kind: 'blocked' } });
471
+ }
472
+ async qrcode() {
473
+ const c = this.c;
474
+ this.require();
475
+ const s = await signed(c, '/api/cas/customer/web/qr-code', { service: CREATOR }, 'POST', { tier: '0101', mnsProfile: 'login_ready', b1Profile: 'login' });
476
+ s.headers['content-type'] = 'application/json';
477
+ const res = await post(c, `${CUSTOMER}/api/cas/customer/web/qr-code`, s.headers, 'casPostNoRate', s.body);
478
+ if (res.status === GATED)
479
+ return null;
480
+ const body = await jsonOf(res);
481
+ const d = body?.data ?? {};
482
+ return body?.success && d.id && d.url ? { id: String(d.id), url: String(d.url) } : null;
483
+ }
484
+ /** 1 成功、2 待扫码、3 待确认、4 过期、0 异常。 */
485
+ async qrcodeStatus(qrId) {
486
+ const c = this.c;
487
+ const api = cspl('/api/cas/customer/web/qr-code', { service: CREATOR, qr_code_id: qrId, source: '' });
488
+ const s = await signed(c, api, '', 'GET', { includeOrigin: true, tier: '0101', mnsProfile: 'login_ready', b1Profile: 'login' });
489
+ const body = await jsonOf(await get(c, CUSTOMER + api, s.headers, 'casGetNoRate'));
490
+ const status = body?.data?.status;
491
+ return status == null ? null : Number(status);
492
+ }
493
+ async sendCode(phone, zone = '86') {
494
+ const c = this.c;
495
+ this.require();
496
+ const s = await signed(c, '/api/cas/customer/web/verify-code', { service: CREATOR, phone, zone }, 'POST', { tier: '0101', mnsProfile: 'login_ready', b1Profile: 'login' });
497
+ s.headers['content-type'] = 'application/json';
498
+ s.headers['x-ratelimit-meta'] = `host=${new URL(CREATOR).host}`;
499
+ const res = await post(c, `${CUSTOMER}/api/cas/customer/web/verify-code`, s.headers, 'casPost', s.body);
500
+ if (res.status === GATED)
501
+ return { ok: false, message: `HTTP ${res.status}` };
502
+ const body = await jsonOf(res);
503
+ return { ok: Boolean(body?.success), message: String(body?.msg ?? body?.message ?? '') };
504
+ }
505
+ async loginByCode(phone, code, zone = '86') {
506
+ const c = this.c;
507
+ this.require();
508
+ const data = { zone, phone, verify_code: code, service: CREATOR, source: '', type: 'phoneVerifyCode' };
509
+ const s = await signed(c, '/api/cas/customer/web/service-ticket', data, 'POST', { tier: '0101', mnsProfile: 'login_ready', b1Profile: 'login' });
510
+ s.headers['content-type'] = 'application/json';
511
+ s.headers['x-ratelimit-meta'] = `host=${new URL(CREATOR).host}`;
512
+ const body = await jsonOf(await post(c, `${CUSTOMER}/api/cas/customer/web/service-ticket`, s.headers, 'casPost', s.body));
513
+ if (!body?.success)
514
+ throw new SpecificAIError('AUTH_REQUIRED', String(body?.msg ?? body?.message ?? '手机号登录失败'));
515
+ }
516
+ /**
517
+ * 上游 XHSCreatorLoginApi.get_user_info:登录页的 user/info 验收。
518
+ * 主站会话桥接时在发布页上发,仍在 0201/nop(mnsProfile 为 null,DSL 那半还没有)。
519
+ */
520
+ async userInfo(o = {}) {
521
+ const c = this.c;
522
+ const s = await signed(c, '/api/galaxy/user/info', '', 'GET', {
523
+ referer: o.referer ?? `${CREATOR}/login`,
524
+ site: 'same-origin',
525
+ trace: true,
526
+ includeOrigin: false,
527
+ tier: o.tier ?? '0101',
528
+ ...(o.mnsProfile === null ? {} : { mnsProfile: o.mnsProfile ?? 'login_ready' }),
529
+ b1Profile: 'login',
530
+ ...(o.dslPairValue ? { dslPairValue: o.dslPairValue } : {}),
531
+ });
532
+ s.headers['content-type'] = 'application/json;charset=UTF-8';
533
+ const body = await jsonOf(await get(c, `${CREATOR}/api/galaxy/user/info`, s.headers, 'loginUserInfo'));
534
+ return body?.success ? (body.data ?? {}) : null;
535
+ }
536
+ }
537
+ // ================================================================ 主站会话 → 创作者中心(上游 XHSCreatorAuth.from_pc_auth)
538
+ /**
539
+ * 从主站带过去的 cookie。主站的 webBuild / xsecappid / loadts 是主站页面的,换成创作者中心的;
540
+ * 主站 cookie 若是从已打开的发布页复制的,会带着创作者中心的 HttpOnly 会话 cookie,一并保留。
541
+ */
542
+ const SHARED_FROM_PC = new Set([
543
+ 'abRequestId', 'ets', 'a1', 'webId', 'unread', 'gid', 'web_session', 'id_token', 'x-rednote-datactry', 'x-rednote-holderctry',
544
+ 'websectiga', 'sec_poison_id', '_gray_did', 'customer-sso-sid', 'x-user-id-creator.xiaohongshu.com', 'customerClientId',
545
+ 'access-token-creator.xiaohongshu.com', 'galaxy_creator_session_id', 'galaxy.creator.beaker.session.id',
546
+ ]);
547
+ /**
548
+ * 用主站的登录态初始化创作者中心,不再单独扫码(AGENTS 5.3)。浏览器从主站点「发布笔记」时就是这样:
549
+ * 发布页导航换出创作者中心的会话 cookie,然后创作者中心跑自己的安全初始化(0201 批次 → user/info → 安装 DS)。
550
+ * 结果写进凭证的 creator scope 和 device.creator,之后的创作者命令直接复用。
551
+ *
552
+ * 桥接在凭证的副本上做,全部成功后才写回:命令失败时 core 照样会把凭证落盘,中途失败(网络、风控、主站也失效)
553
+ * 若已经改了 creator scope,下次就会拿这份半成品当创作者中心的登录态用,再也不会重新桥接。
554
+ */
555
+ export async function creatorFromPc(ctx) {
556
+ const real = ctx.credential;
557
+ const temp = { ...real, scopes: { ...real.scopes, creator: { cookies: [], tokens: {} } }, device: { ...real.device }, extra: { ...real.extra } };
558
+ delete temp.device.creator;
559
+ const bridge = { ...ctx, credential: temp };
560
+ const main = scope(real, 'main').cookies;
561
+ const cookies = new CookieJar(temp.scopes.creator.cookies);
562
+ // 各子域的 acw_tc(host-only)照浏览器的 cookie 罐一起带过去,发送时排在共享 cookie 前面
563
+ for (const k of main)
564
+ if (k.name === 'acw_tc' && !k.domain.startsWith('.'))
565
+ cookies.cookies.push({ ...k });
566
+ const seen = new Set();
567
+ for (const k of main) {
568
+ if (!SHARED_FROM_PC.has(k.name) || !k.value || seen.has(k.name))
569
+ continue;
570
+ seen.add(k.name);
571
+ cookies.set(k.name, k.value, COOKIE_DOMAIN);
572
+ }
573
+ if (!seen.has('_gray_did'))
574
+ cookies.set('_gray_did', rand.uuid4(), COOKIE_DOMAIN);
575
+ cookies.set('webBuild', CREATOR_REFERENCE.release.webBuild, COOKIE_DOMAIN);
576
+ cookies.set('xsecappid', 'ugc', COOKIE_DOMAIN);
577
+ cookies.set('loadts', String(rand.now()), COOKIE_DOMAIN);
578
+ const c = new Creator(bridge);
579
+ // 主站的 websectiga / gid 会让签名状态以为安全初始化已完成;创作者中心还要装自己的 DS 程序
580
+ c.state.securityReady = false;
581
+ c.state.dsl = '';
582
+ c.state.dsProgram = '';
583
+ const l = new CreatorLogin(c);
584
+ await l.publishNavigation();
585
+ await l.honeypot();
586
+ await l.bootstrap(false);
587
+ const info = await l.userInfo({
588
+ tier: '0201',
589
+ mnsProfile: null,
590
+ dslPairValue: `${c.state.loadts};undefined`,
591
+ referer: `${CREATOR}/publish/publish?source=official`,
592
+ });
593
+ if (!info)
594
+ throw authError(ctx, '创作者中心不认主站的登录态,请重新登录');
595
+ await l.activate();
596
+ try {
597
+ l.require();
598
+ }
599
+ catch {
600
+ await l.completeSecurity();
601
+ }
602
+ c.save();
603
+ // 成功:写回正式凭证(cookie 罐引用的就是这个 scope 的数组),会话从此改用正式凭证
604
+ real.scopes.creator = temp.scopes.creator;
605
+ real.device.creator = temp.device.creator;
606
+ Object.assign(real.extra, temp.extra);
607
+ bridge.credential = real;
608
+ return c;
609
+ }