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,775 @@
1
+ import { SpecificAIError } from '../../../core/errors.js';
2
+ import { parseJsonp } from '../../../core/http.js';
3
+ import { unescapeHtml } from '../../../core/normalize.js';
4
+ import { jsonLoads, pyFloatStr } from '../../../core/py.js';
5
+ import * as rand from '../../../core/rand.js';
6
+ import { encryptQuery } from './aks.js';
7
+ import { solveCaptcha } from './jcap.js';
8
+ import { summerEncrypt } from './js.js';
9
+ import { APPID_PASSPORT, basic, LOGIN_PAGE, PASSPORT_URL, QR_URL, qrImage, qrJsonp, qrValidation } from './profile.js';
10
+ import { encodeQueryPairs, parseAttrs, randomJqueryCallback, TraceContext } from './util.js';
11
+ /**
12
+ * 京东 PC 登录(上游 jd_apis/jd_login_api.py 与 jd_sms_login_api.py):
13
+ * 扫码三步(show → check → qrCodeTicketValidation),手机号短信(JCAP → sendMessage → loginService,必要时额外安全验证)。
14
+ * passport 的 query / body 由 aks.js 加密成 aksParamsU / aksParamsB。
15
+ */
16
+ export const APPID = 133;
17
+ const PAGE_SOURCE = 'login2025';
18
+ const PAGE_LOCATION = '';
19
+ const FIRST_SHOW = 'f';
20
+ const RETURN_URL = 'https://home.jd.com/index.html';
21
+ const SMS_CAPTCHA_APP_ID = '1000802';
22
+ const SAFE_LOGIN_SMS_TYPES = new Set(['DANGEROUS_DOWN', 'PARENT_DANGEROUS_DOWN', 'HISTORY_MOBILE']);
23
+ /** 登录页 getAliveSsoDomains() 的结果。 */
24
+ export const SSO_DOMAINS = [
25
+ 'sso.jd.hk', 'sso.jkcsjd.com', 'sso.healthjd.com', 'sso.jingxi.com', 'sso.jdh.com', 'sso.jingdong.com', 'ssa.7fresh.com',
26
+ 'sso.jdpay.com', 'sso.jingdonghealth.cn', 'sso.vipmro.com', 'sso.yiyaojd.com', 'sso.jdcloud.com', 'sso.jdl.com', 'sso.jhscm.com',
27
+ 'sso.jddj.com',
28
+ ].join(',');
29
+ const aksState = (jd) => (jd.ctx.credential.device.aks ??= {});
30
+ /** 响应体:JSON,或括号包着的 JSON(_parse_response)。 */
31
+ export async function parseResponse(res) {
32
+ const text = await res.text();
33
+ try {
34
+ const v = jsonLoads(text);
35
+ if (v && typeof v === 'object' && !Array.isArray(v))
36
+ return v;
37
+ }
38
+ catch { }
39
+ let t = (text ?? '').trim().replace(/;+$/, '');
40
+ if (t.startsWith('(') && t.endsWith(')'))
41
+ t = t.slice(1, -1).trim();
42
+ try {
43
+ const v = jsonLoads(t);
44
+ return v && typeof v === 'object' && !Array.isArray(v) ? v : {};
45
+ }
46
+ catch {
47
+ return { _invalid_response_length: t.length };
48
+ }
49
+ }
50
+ // ================================================================ 扫码
51
+ /** 加载登录页,拿页面下发的 `#uuid` 建立 JDAS 链路上下文(_prepare_trace_context)。 */
52
+ export async function prepareTrace(jd) {
53
+ const res = await jd.send({ url: LOGIN_PAGE, headers: basic('DOC').get(), timeout: 15 });
54
+ if (res.status !== 200)
55
+ throw new SpecificAIError('UPSTREAM', `登录页初始化失败 HTTP ${res.status}`);
56
+ const html = await res.text();
57
+ try {
58
+ return { trace: TraceContext.fromHtml(html), html };
59
+ }
60
+ catch (err) {
61
+ throw new SpecificAIError('UPSTREAM', err.message);
62
+ }
63
+ }
64
+ /** 取二维码(get_qrcode):返回 PNG 与二维码内容(`https://qr.m.jd.com/p?k=<QRCodeKey>&appid=133`)。 */
65
+ export async function getQrcode(jd, size = 147) {
66
+ const res = await jd.send({
67
+ url: `${QR_URL}/show`,
68
+ headers: qrImage().get(),
69
+ query: [
70
+ ['appid', APPID],
71
+ ['size', size],
72
+ ['t', rand.now()],
73
+ ],
74
+ timeout: 15,
75
+ });
76
+ const png = new Uint8Array(await res.arrayBuffer());
77
+ if (res.status !== 200 || !png.length)
78
+ throw new SpecificAIError('UPSTREAM', `取二维码失败 HTTP ${res.status}`);
79
+ if (!jd.cookie('wlfstk_smdl'))
80
+ throw new SpecificAIError('UPSTREAM', '响应里没有 wlfstk_smdl,无法轮询');
81
+ const key = jd.cookie('QRCodeKey');
82
+ return { png, content: key ? `${QR_URL}/p?k=${key}&appid=${APPID}` : null };
83
+ }
84
+ /** 轮询一次扫码状态(check_qrcode):201 未扫、202 已扫待确认、200 带 ticket。 */
85
+ export async function checkQrcode(jd) {
86
+ const res = await jd.send({
87
+ url: `${QR_URL}/check`,
88
+ headers: qrJsonp().get(),
89
+ query: [
90
+ ['callback', randomJqueryCallback()],
91
+ ['appid', APPID],
92
+ ['token', jd.cookie('wlfstk_smdl') ?? ''],
93
+ ['_', rand.now()],
94
+ ],
95
+ timeout: 15,
96
+ });
97
+ const text = await res.text();
98
+ return parseJsonp(text) ?? { code: -1, raw: text };
99
+ }
100
+ /** passport 的 aks.js 公钥(_public_key)。 */
101
+ export async function publicKey(jd, trace) {
102
+ const h = qrValidation(trace).set('accept', '*/*');
103
+ const res = await jd.send({ url: `${PASSPORT_URL}/publicKey/init`, headers: h.get(), timeout: 15 });
104
+ const text = await res.text();
105
+ let payload;
106
+ try {
107
+ payload = jsonLoads(text);
108
+ }
109
+ catch {
110
+ throw new SpecificAIError('UPSTREAM', `publicKey/init 返回非 JSON:${text.slice(0, 200)}`);
111
+ }
112
+ const key = payload && typeof payload === 'object' ? payload.data : null;
113
+ if (!key)
114
+ throw new SpecificAIError('UPSTREAM', 'publicKey/init 缺少 data');
115
+ return String(key);
116
+ }
117
+ /** 用 ticket 换 thor / pin(validate_ticket)。 */
118
+ export async function validateTicket(jd, ticket, trace) {
119
+ if (!ticket)
120
+ throw new SpecificAIError('UPSTREAM', 'ticket 为空');
121
+ jd.h5st.configure(jd.cookieStr, PASSPORT_URL, LOGIN_PAGE);
122
+ const signed = await jd.h5st.sign({ t: ticket }, APPID_PASSPORT);
123
+ const h5st = String(signed.h5st ?? '');
124
+ const stk = String(signed._stk ?? '');
125
+ if (!h5st || !stk)
126
+ throw new SpecificAIError('ERROR', 'passport h5st/_stk 生成失败');
127
+ const pairs = [
128
+ ['t', ticket],
129
+ ['pageSource', PAGE_SOURCE],
130
+ ['pageLocation', PAGE_LOCATION],
131
+ ['ReturnUrl', RETURN_URL],
132
+ ['h5st', h5st],
133
+ ['_stk', stk],
134
+ ['firstShowAccountLoginPage', FIRST_SHOW],
135
+ ['ssoDomains', SSO_DOMAINS],
136
+ ];
137
+ const encoded = encodeQueryPairs(pairs);
138
+ const key = await publicKey(jd, trace.nextHeaders());
139
+ const encrypted = encryptQuery(encoded, key, aksState(jd));
140
+ const h = qrValidation(trace.nextHeaders()).referer(LOGIN_PAGE);
141
+ const res = await jd.send({ url: `${PASSPORT_URL}/uc/qrCodeTicketValidation`, headers: h.get(), query: [['aksParamsU', encrypted]], timeout: 15 });
142
+ const text = await res.text();
143
+ let json;
144
+ try {
145
+ json = jsonLoads(text);
146
+ }
147
+ catch {
148
+ json = { raw: text.slice(0, 500) };
149
+ }
150
+ if (json && typeof json === 'object' && 'returnCode' in json && json.returnCode !== 0 && json.returnCode !== '0') {
151
+ throw new SpecificAIError('UPSTREAM', `ticket 兑换失败 returnCode=${json.returnCode}`, { detail: { returnCode: json.returnCode } });
152
+ }
153
+ if (!jd.cookie('thor'))
154
+ throw new SpecificAIError('UPSTREAM', 'ticket 兑换未拿到 thor');
155
+ return json;
156
+ }
157
+ /** 登录页的隐藏域(parse_login_inputs):按 id 与 name 记录 value。 */
158
+ export function parseLoginInputs(html) {
159
+ const values = {};
160
+ for (const tag of html.match(/<input\b[^>]*>/gi) ?? []) {
161
+ const attrs = parseAttrs(tag);
162
+ const value = attrs.value ?? '';
163
+ for (const key of [attrs.id, attrs.name])
164
+ if (key)
165
+ values[key] = value;
166
+ }
167
+ if (!values.uuid)
168
+ throw new SpecificAIError('UPSTREAM', '登录页缺少 uuid');
169
+ return values;
170
+ }
171
+ /** 手机号规范化(normalize_mobile)。 */
172
+ export function normalizeMobile(mobile, areaCode = '0086') {
173
+ const digits = String(mobile ?? '').replace(/[\s()-]/g, '');
174
+ if (!/^\+?\d{6,30}$/.test(digits))
175
+ throw new SpecificAIError('USAGE', '手机号格式不正确');
176
+ const area = String(areaCode ?? '0086').replace(/[\s()-]/g, '');
177
+ if (['86', '+86', '0086'].includes(area)) {
178
+ let local = digits.replace(/^\++/, '');
179
+ if (local.startsWith('0086'))
180
+ local = local.slice(4);
181
+ else if (local.startsWith('86') && local.length > 11)
182
+ local = local.slice(2);
183
+ return local;
184
+ }
185
+ if (!/^(?:00|\+)?\d{1,6}$/.test(area))
186
+ throw new SpecificAIError('USAGE', '国家/地区代码格式不正确');
187
+ const prefix = area.startsWith('00') ? area.slice(2) : area.replace(/^\++/, '');
188
+ let local = digits.replace(/^\++/, '');
189
+ if (local.startsWith('00' + prefix))
190
+ local = local.slice(2 + prefix.length);
191
+ else if (local.startsWith(prefix))
192
+ local = local.slice(prefix.length);
193
+ return `+${prefix}${local}`;
194
+ }
195
+ /** ITU-T E.164 的一位、两位国家/地区码;其余都是三位(国家码是前缀码,按前缀即可切分)。 */
196
+ const CALLING_1 = new Set(['1', '7']);
197
+ const CALLING_2 = new Set('20 27 30 31 32 33 34 36 39 40 41 43 44 45 46 47 48 49 51 52 53 54 55 56 57 58 60 61 62 63 64 65 66 81 82 84 86 90 91 92 93 94 95 98'.split(' '));
198
+ /** `+852…` / `00852…` 开头的号码 → 国家/地区码(`852`);不带国际前缀时为 null。 */
199
+ export function callingCode(mobile) {
200
+ const m = /^(?:\+|00)(\d+)$/.exec(String(mobile ?? '').replace(/[\s()-]/g, ''));
201
+ if (!m)
202
+ return null;
203
+ const d = m[1];
204
+ if (CALLING_1.has(d.slice(0, 1)))
205
+ return d.slice(0, 1);
206
+ if (CALLING_2.has(d.slice(0, 2)))
207
+ return d.slice(0, 2);
208
+ return d.slice(0, 3);
209
+ }
210
+ /**
211
+ * `--phone` → 登录接口里的手机号:带 `+` / `00` 国际前缀时按前缀识别国家/地区码,
212
+ * 再交给 normalize_mobile(等于上游 login(area_code=<该码>));否则按中国大陆(0086)。
213
+ */
214
+ export function loginMobile(phone) {
215
+ const code = callingCode(phone);
216
+ return normalizeMobile(phone, code ? `+${code}` : '0086');
217
+ }
218
+ /** passport 的 jQuery AJAX 头(_ajax_headers)。 */
219
+ function ajaxHeaders(ctx, accept = 'application/json, text/javascript, */*; q=0.01', form = false) {
220
+ const h = qrValidation(ctx.trace.nextHeaders()).referer(LOGIN_PAGE).set('accept', accept);
221
+ if (form) {
222
+ h.set('content-type', 'application/x-www-form-urlencoded; charset=UTF-8');
223
+ h.set('origin', PASSPORT_URL);
224
+ }
225
+ const order = ['sgm-context', 'sec-ch-ua-platform', 'jdas-trace-id', 'referer', 'sec-ch-ua', 'sec-ch-ua-mobile', 'x-requested-with', 'user-agent', 'accept', 'jdas-page-id'];
226
+ if (form)
227
+ order.push('content-type');
228
+ order.push('jdas-session-id', 'accept-encoding', 'accept-language');
229
+ if (form)
230
+ order.push('origin');
231
+ order.push('priority', 'sec-fetch-dest', 'sec-fetch-mode', 'sec-fetch-site');
232
+ return h.reorder(order);
233
+ }
234
+ const encrypted = (jd, ctx, pairs) => encryptQuery(encodeQueryPairs(pairs), ctx.publicKey, aksState(jd));
235
+ /** 登录页首屏 QR 图片请求:只要它下发的 wlfstk_smdl(_load_qr_cookie)。 */
236
+ async function loadQrCookie(jd) {
237
+ const res = await jd.send({
238
+ url: `${QR_URL}/show`,
239
+ headers: qrImage().get(),
240
+ query: [
241
+ ['appid', APPID],
242
+ ['size', 147],
243
+ ['t', rand.now()],
244
+ ],
245
+ timeout: 15,
246
+ });
247
+ const token = res.headers
248
+ .getSetCookie()
249
+ .map((l) => /^\s*wlfstk_smdl=([^;]*)/.exec(l)?.[1])
250
+ .find((v) => v);
251
+ if (res.status !== 200 || !token)
252
+ throw new SpecificAIError('UPSTREAM', `登录页 QR 设备 Cookie 初始化失败 HTTP ${res.status}`);
253
+ jd.update([['wlfstk_smdl', token]]);
254
+ }
255
+ /** ssoDomain.js:取动态列表,保留 /alive 返回 success 的域(_load_sso_domains)。 */
256
+ async function loadSsoDomains(jd, ctx) {
257
+ const res = await jd.send({
258
+ url: `${PASSPORT_URL}/ssoDomain/getList`,
259
+ headers: ajaxHeaders(ctx).get(),
260
+ query: [
261
+ ['ReturnUrl', RETURN_URL],
262
+ ['r', pyFloatStr(rand.random())],
263
+ ],
264
+ timeout: 15,
265
+ });
266
+ let candidates = [];
267
+ try {
268
+ candidates = await res.json();
269
+ }
270
+ catch { }
271
+ const list = Array.isArray(candidates) ? candidates.slice(0, 32).map(String).filter((d) => /^[A-Za-z0-9.-]{3,253}$/.test(d)) : [];
272
+ const results = await Promise.all(list.map(async (domain) => {
273
+ const callback = randomJqueryCallback();
274
+ const h = qrJsonp().referer(LOGIN_PAGE);
275
+ try {
276
+ const probe = await jd.http.request({
277
+ url: `https://${domain}/alive`,
278
+ headers: h.get(),
279
+ query: [
280
+ ['callback', callback],
281
+ ['_', rand.now()],
282
+ ],
283
+ cookies: false,
284
+ timeout: 8,
285
+ });
286
+ const text = await probe.text();
287
+ let outer = parseJsonp(text);
288
+ if (outer == null) {
289
+ const m = /^[^(]*\(([\s\S]*)\)[;\s]*$/.exec(text);
290
+ const quoted = m ? m[1].trim() : '';
291
+ if (quoted.length >= 2 && quoted[0] === quoted[quoted.length - 1] && (quoted[0] === "'" || quoted[0] === '"'))
292
+ outer = quoted.slice(1, -1).replaceAll("\\'", "'");
293
+ }
294
+ const payload = typeof outer === 'string' ? jsonLoads(outer) : outer;
295
+ const ok = probe.status === 200 && payload && typeof payload === 'object' && payload.result === 'success';
296
+ return { domain: ok ? domain : '', probe };
297
+ }
298
+ catch {
299
+ return { domain: '', probe: null };
300
+ }
301
+ }));
302
+ const reachable = [];
303
+ for (const { domain, probe } of results) {
304
+ if (probe)
305
+ jd.absorb(probe);
306
+ if (domain)
307
+ reachable.push(domain);
308
+ }
309
+ ctx.fields.ssoDomains = reachable.join(',');
310
+ }
311
+ /** 登录行为序列 id(_load_seq_sid)。 */
312
+ async function loadSeqSid(jd, ctx) {
313
+ const res = await jd.send({
314
+ url: 'https://seq.jd.com/jseqf.html',
315
+ headers: qrJsonp().referer(LOGIN_PAGE).get(),
316
+ query: [
317
+ ['bizId', 'passport_jd_com_login_pc'],
318
+ ['platform', 'js'],
319
+ ['version', '1'],
320
+ ],
321
+ timeout: 15,
322
+ });
323
+ if (res.status !== 200)
324
+ throw new SpecificAIError('UPSTREAM', `登录行为序列初始化失败 HTTP ${res.status}`);
325
+ const m = /_jdtdmap_sessionId\s*=\s*["'](\d+)["']/.exec(await res.text());
326
+ if (!m)
327
+ throw new SpecificAIError('UPSTREAM', 'seq.jd.com 响应缺少 _jdtdmap_sessionId');
328
+ ctx.fields.seqSid = m[1];
329
+ }
330
+ /** 初始化短信图形验证码(refresh_captcha)。 */
331
+ export async function refreshCaptcha(jd, ctx) {
332
+ const enc = encrypted(jd, ctx, [['appId', SMS_CAPTCHA_APP_ID]]);
333
+ const res = await jd.send({ url: `${PASSPORT_URL}/uc/graphic/sessionId/refresh`, headers: ajaxHeaders(ctx).get(), query: [['aksParamsU', enc]], timeout: 15 });
334
+ const payload = await parseResponse(res);
335
+ if (res.status !== 200 || !(payload.code === 1 || payload.code === '1')) {
336
+ throw new SpecificAIError('UPSTREAM', `短信图形验证码初始化失败 HTTP ${res.status} code=${payload.code}`);
337
+ }
338
+ ctx.captchaStatus = Number.parseInt(String(payload.status ?? 0), 10) || 0;
339
+ ctx.captchaSessionId = String(payload.sessionId ?? '');
340
+ ctx.captchaJwtToken = String(payload.jwtToken ?? '');
341
+ if (ctx.captchaStatus === 1 && (!ctx.captchaSessionId || !ctx.captchaJwtToken))
342
+ throw new SpecificAIError('UPSTREAM', '短信图形验证码响应缺少 sessionId/jwtToken');
343
+ return payload;
344
+ }
345
+ /** 手机号登录的准备(JdSmsLoginAPI.start):登录页、QR cookie、公钥、设备参数、SSO 域、行为序列、图形验证码。 */
346
+ export async function startSms(jd) {
347
+ const res = await jd.send({ url: LOGIN_PAGE, headers: basic('DOC').get(), timeout: 15 });
348
+ if (res.status !== 200)
349
+ throw new SpecificAIError('UPSTREAM', `登录页初始化失败 HTTP ${res.status}`);
350
+ const html = await res.text();
351
+ const fields = parseLoginInputs(html);
352
+ const ctx = { trace: TraceContext.fromHtml(html), fields, publicKey: '', captchaStatus: 0, captchaSessionId: '', captchaJwtToken: '' };
353
+ await loadQrCookie(jd);
354
+ ctx.publicKey = await publicKey(jd, ctx.trace.nextHeaders());
355
+ const device = await jd.refreshDevice();
356
+ Object.assign(fields, device);
357
+ fields.sessionId = device.fp;
358
+ await loadSsoDomains(jd, ctx);
359
+ await loadSeqSid(jd, ctx);
360
+ await refreshCaptcha(jd, ctx);
361
+ return ctx;
362
+ }
363
+ /** 发短信(send_code)。 */
364
+ export async function sendCode(jd, ctx, mobile, verifyToken = '') {
365
+ if (ctx.captchaStatus === 1 && !verifyToken)
366
+ return { success: false, message: '发送短信前必须完成人机验证', payload: {} };
367
+ const f = ctx.fields;
368
+ const pairs = [
369
+ ['source', f.source ?? ''],
370
+ ['eid', f.eid ?? ''],
371
+ ['uuid', f.uuid ?? ''],
372
+ ['mobile', mobile],
373
+ ['imageAuthCodeToken', ''],
374
+ ['firstShowAccountLoginPage', f.firstShowAccountLoginPage ?? FIRST_SHOW],
375
+ ['pageSource', f.pageSource ?? PAGE_SOURCE],
376
+ ['pageLocation', f.pageLocation ?? PAGE_LOCATION],
377
+ ];
378
+ if (ctx.captchaSessionId)
379
+ pairs.push(['graphicCaptchaSessionId', ctx.captchaSessionId]);
380
+ if (ctx.captchaJwtToken)
381
+ pairs.push(['graphicCaptchaJwtToken', ctx.captchaJwtToken]);
382
+ if (verifyToken)
383
+ pairs.push(['graphicCaptchaVerifyToken', verifyToken]);
384
+ pairs.push(['_', rand.now()]);
385
+ const enc = encrypted(jd, ctx, pairs);
386
+ const res = await jd.send({ url: `${PASSPORT_URL}/uc/mobile/sendMessage`, headers: ajaxHeaders(ctx, '*/*').get(), query: [['aksParamsU', enc]], timeout: 15 });
387
+ const payload = await parseResponse(res);
388
+ const success = res.status === 200 && (payload.code === 1 || payload.code === '1');
389
+ const message = String(payload.msg || (success ? '短信已发送' : '短信发送失败')).replaceAll(mobile, '<mobile>');
390
+ return { success, message, payload };
391
+ }
392
+ /** 用短信码换登录态(submit_code)。成功时跟随跳转拿到 thor / pin。 */
393
+ export async function submitCode(jd, ctx, mobile, smsCode) {
394
+ if (!/^\d{6}$/.test(String(smsCode ?? '')))
395
+ return { success: false, message: '短信验证码必须是六位数字', payload: {} };
396
+ const f = ctx.fields;
397
+ const device = {
398
+ eid: f.eid || jd.cookie('3AB9D23F7A4B3C9B') || '',
399
+ eid2: f.eid2 || jd.cookie('3AB9D23F7A4B3CSS') || '',
400
+ fp: f.fp ?? '',
401
+ };
402
+ jd.h5st.configure(jd.cookieStr, PASSPORT_URL, LOGIN_PAGE);
403
+ const signed = await jd.h5st.sign({ mobile }, APPID_PASSPORT);
404
+ if (!signed.h5st || signed._stk !== 'mobile')
405
+ return { success: false, message: '手机号登录 h5st/_stk 生成失败', payload: {} };
406
+ const body = [
407
+ ['uuid', f.uuid ?? ''],
408
+ ['eid', device.eid],
409
+ ['eid2', device.eid2],
410
+ ['fp', device.fp],
411
+ ['_t', f._t ?? '_t'],
412
+ ['mobile', mobile],
413
+ ['mobileCode', smsCode],
414
+ ['pageSource', f.pageSource ?? PAGE_SOURCE],
415
+ ['pageLocation', f.pageLocation ?? PAGE_LOCATION],
416
+ ['loginType', f.loginType ?? 'f'],
417
+ ['sa_token', f.sa_token ?? ''],
418
+ ['seqSid', f.seqSid ?? ''],
419
+ ['useSlideAuthCode', f.useRandomSlideAuthCode ?? ''],
420
+ ['authcode', ''],
421
+ ['ssoDomains', f.ssoDomains ?? ''],
422
+ ['h5st', String(signed.h5st)],
423
+ ['_stk', String(signed._stk)],
424
+ ];
425
+ const query = `ReturnUrl=${RETURN_URL}&r=${pyFloatStr(rand.random())}&version=2015`;
426
+ const encUrl = encryptQuery(query, ctx.publicKey, aksState(jd));
427
+ const encBody = encrypted(jd, ctx, body);
428
+ const res = await jd.send({
429
+ method: 'POST',
430
+ url: `${PASSPORT_URL}/uc/mobile/loginService`,
431
+ headers: ajaxHeaders(ctx, 'text/plain, */*; q=0.01', true).get(),
432
+ query: [['aksParamsU', encUrl]],
433
+ form: [['aksParamsB', encBody]],
434
+ timeout: 15,
435
+ });
436
+ const payload = await parseResponse(res);
437
+ if (payload._t)
438
+ f._t = String(payload._t);
439
+ const target = payload.success || payload.transfer;
440
+ if (target) {
441
+ const resolved = new URL(String(target), LOGIN_PAGE);
442
+ if (resolved.protocol !== 'http:' && resolved.protocol !== 'https:')
443
+ return { success: false, message: '登录成功跳转地址协议异常', payload };
444
+ await follow(jd, resolved.href, basic('DOC').set('referer', LOGIN_PAGE));
445
+ if (jd.isLogin)
446
+ return { success: true, message: '手机号短信登录成功', payload };
447
+ return { success: false, message: '登录跳转完成,但未取得 thor/pin', payload };
448
+ }
449
+ for (const key of ['username', 'authcode2', 'emptyAuthcode', 'pwd', 'msg']) {
450
+ if (payload[key])
451
+ return { success: false, message: String(payload[key]).replaceAll(mobile, '<mobile>'), payload };
452
+ }
453
+ if (payload.newSafeVerify)
454
+ return { success: false, message: '账号需要额外安全验证', payload };
455
+ return { success: false, message: `手机号登录失败 HTTP ${res.status}`, payload };
456
+ }
457
+ /** 逐跳跟随重定向并吸收每一跳的 Set-Cookie(allow_redirects=True + absorb_response)。 */
458
+ export async function follow(jd, url, headers, max = 10) {
459
+ let current = url;
460
+ for (let i = 0;; i++) {
461
+ const res = await jd.send({ url: current, headers: headers.get(), redirect: 'manual', timeout: 20 });
462
+ const location = res.headers.get('location');
463
+ if (res.status < 300 || res.status >= 400 || !location || i >= max)
464
+ return res;
465
+ current = new URL(location, current).href;
466
+ }
467
+ }
468
+ /** 找出 `window.safeWebConfig = ...` 赋值的对象(_extract_safe_web_config),不执行 JS。 */
469
+ export function extractSafeWebConfig(html) {
470
+ const source = html ?? '';
471
+ const pattern = /(?:(?:window\s*\.\s*)?safeWebConfig|(?:var|let|const)\s+safeWebConfig)\s*=\s*/g;
472
+ for (const m of source.matchAll(pattern)) {
473
+ const value = parseCandidate(source.slice(m.index + m[0].length).trimStart());
474
+ if (value && Object.keys(value).length)
475
+ return value;
476
+ }
477
+ return {};
478
+ }
479
+ function parseCandidate(candidate) {
480
+ for (const variant of [candidate, unescapeHtml(candidate)]) {
481
+ const v = variant.trimStart();
482
+ try {
483
+ const obj = parseJsLiteral(v);
484
+ if (obj && typeof obj === 'object' && !Array.isArray(obj))
485
+ return obj;
486
+ }
487
+ catch { }
488
+ const pm = /^JSON\s*\.\s*parse\s*\(\s*([\s\S]*)/.exec(v);
489
+ if (!pm)
490
+ continue;
491
+ let arg = pm[1].trimStart();
492
+ let transform = '';
493
+ const cm = /^(decodeURIComponent|decodeURI|atob)\s*\(\s*([\s\S]*)/.exec(arg);
494
+ if (cm) {
495
+ transform = cm[1];
496
+ arg = cm[2];
497
+ }
498
+ let serialized;
499
+ try {
500
+ serialized = new JsParser(arg).leadingString();
501
+ }
502
+ catch {
503
+ continue;
504
+ }
505
+ if (transform === 'decodeURIComponent' || transform === 'decodeURI')
506
+ serialized = decodeURIComponent(serialized);
507
+ else if (transform === 'atob')
508
+ serialized = Buffer.from(serialized, 'base64').toString('utf8');
509
+ try {
510
+ const obj = jsonLoads(serialized);
511
+ if (obj && typeof obj === 'object' && !Array.isArray(obj) && Object.keys(obj).length)
512
+ return obj;
513
+ }
514
+ catch { }
515
+ }
516
+ return null;
517
+ }
518
+ /** 受限的 JS / JSON5 数据字面量解析(_parse_js_data_object):只认字面量,拒绝任何可执行的标识符。 */
519
+ class JsParser {
520
+ text;
521
+ i = 0;
522
+ constructor(text) {
523
+ this.text = text;
524
+ }
525
+ skip() {
526
+ for (;;) {
527
+ const c = this.text[this.i];
528
+ if (c !== undefined && /\s/.test(c))
529
+ this.i++;
530
+ else if (this.text.startsWith('//', this.i)) {
531
+ const end = this.text.indexOf('\n', this.i + 2);
532
+ this.i = end < 0 ? this.text.length : end + 1;
533
+ }
534
+ else if (this.text.startsWith('/*', this.i)) {
535
+ const end = this.text.indexOf('*/', this.i + 2);
536
+ if (end < 0)
537
+ throw new Error('unterminated comment');
538
+ this.i = end + 2;
539
+ }
540
+ else
541
+ return;
542
+ }
543
+ }
544
+ leadingString() {
545
+ this.skip();
546
+ const q = this.text[this.i];
547
+ if (q !== "'" && q !== '"')
548
+ throw new Error('string expected');
549
+ return this.string();
550
+ }
551
+ string() {
552
+ const quote = this.text[this.i];
553
+ let out = '';
554
+ this.i++;
555
+ while (this.i < this.text.length) {
556
+ const c = this.text[this.i++];
557
+ if (c === quote)
558
+ return out;
559
+ if (c !== '\\') {
560
+ out += c;
561
+ continue;
562
+ }
563
+ const e = this.text[this.i++];
564
+ if (e === undefined)
565
+ break;
566
+ const simple = { n: '\n', r: '\r', t: '\t', b: '\b', f: '\f', v: '\v', '0': '\0' };
567
+ if (e in simple)
568
+ out += simple[e];
569
+ else if (e === 'u') {
570
+ out += String.fromCharCode(parseInt(this.text.slice(this.i, this.i + 4), 16));
571
+ this.i += 4;
572
+ }
573
+ else if (e === 'x') {
574
+ out += String.fromCharCode(parseInt(this.text.slice(this.i, this.i + 2), 16));
575
+ this.i += 2;
576
+ }
577
+ else if (e === '\n') {
578
+ }
579
+ else
580
+ out += e;
581
+ }
582
+ throw new Error('unterminated string');
583
+ }
584
+ identifier() {
585
+ const m = /^[A-Za-z_$][A-Za-z0-9_$]*/.exec(this.text.slice(this.i));
586
+ if (!m)
587
+ throw new Error('identifier expected');
588
+ this.i += m[0].length;
589
+ return m[0];
590
+ }
591
+ value() {
592
+ this.skip();
593
+ const c = this.text[this.i];
594
+ if (c === undefined)
595
+ throw new Error('value expected');
596
+ if (c === '{')
597
+ return this.object();
598
+ if (c === '[')
599
+ return this.array();
600
+ if (c === "'" || c === '"')
601
+ return this.string();
602
+ const num = /^[-+]?(?:0[xX][0-9A-Fa-f]+|(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?)/.exec(this.text.slice(this.i));
603
+ if (num) {
604
+ this.i += num[0].length;
605
+ return Number(num[0]);
606
+ }
607
+ const name = this.identifier();
608
+ const constants = { true: true, false: false, null: null, undefined: null };
609
+ if (!(name in constants))
610
+ throw new Error('executable identifier rejected');
611
+ return constants[name];
612
+ }
613
+ object() {
614
+ const out = {};
615
+ this.i++;
616
+ for (;;) {
617
+ this.skip();
618
+ if (this.i >= this.text.length)
619
+ throw new Error('unterminated object');
620
+ if (this.text[this.i] === '}') {
621
+ this.i++;
622
+ return out;
623
+ }
624
+ const c = this.text[this.i];
625
+ const key = c === "'" || c === '"' ? this.string() : this.identifier();
626
+ this.skip();
627
+ if (this.text[this.i] !== ':')
628
+ throw new Error('colon expected');
629
+ this.i++;
630
+ out[key] = this.value();
631
+ this.skip();
632
+ if (this.text[this.i] === ',')
633
+ this.i++;
634
+ else if (this.text[this.i] !== '}')
635
+ throw new Error('comma expected');
636
+ }
637
+ }
638
+ array() {
639
+ const out = [];
640
+ this.i++;
641
+ for (;;) {
642
+ this.skip();
643
+ if (this.i >= this.text.length)
644
+ throw new Error('unterminated array');
645
+ if (this.text[this.i] === ']') {
646
+ this.i++;
647
+ return out;
648
+ }
649
+ out.push(this.value());
650
+ this.skip();
651
+ if (this.text[this.i] === ',')
652
+ this.i++;
653
+ else if (this.text[this.i] !== ']')
654
+ throw new Error('comma expected');
655
+ }
656
+ }
657
+ }
658
+ function parseJsLiteral(text) {
659
+ const p = new JsParser(text);
660
+ p.skip();
661
+ if (text[p.i] !== '{')
662
+ throw new Error('top-level object expected');
663
+ return p.value();
664
+ }
665
+ const SAFE_HOSTS = ['jd.com', 'jdpay.com', 'jingdong.com'];
666
+ const safeHost = (host) => SAFE_HOSTS.some((s) => host === s || host.endsWith('.' + s));
667
+ /** 在同一会话里打开官方 safeVerifyUrl(load_safe_verify)。 */
668
+ export async function loadSafeVerify(jd, payload) {
669
+ const target = new URL(String(payload?.safeVerifyUrl ?? ''), LOGIN_PAGE);
670
+ if (target.protocol !== 'https:' || !safeHost(target.hostname.toLowerCase())) {
671
+ throw new SpecificAIError('UPSTREAM', `额外安全验证跳转地址异常:host=${target.hostname || '<empty>'}`);
672
+ }
673
+ const res = await follow(jd, target.href, basic('DOC').set('referer', LOGIN_PAGE));
674
+ if (res.status !== 200)
675
+ throw new SpecificAIError('UPSTREAM', `额外安全验证页面加载失败 HTTP ${res.status}`);
676
+ const finalUrl = res.url || target.href;
677
+ if (!safeHost(new URL(finalUrl).hostname.toLowerCase()))
678
+ throw new SpecificAIError('UPSTREAM', '额外安全验证页面重定向域异常');
679
+ const html = await res.text();
680
+ return { finalUrl, config: extractSafeWebConfig(html), requestParams: Object.fromEntries(target.searchParams) };
681
+ }
682
+ function safeHeaders(jd, page, form = false) {
683
+ const u = new URL(page.finalUrl);
684
+ const h = basic('DOC').set('accept', 'application/json, text/plain, */*').set('referer', page.finalUrl).set('origin', `${u.protocol}//${u.host}`).set('x-requested-with', 'XMLHttpRequest');
685
+ if (form)
686
+ h.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
687
+ const xsrf = jd.cookie('XSRF-TOKEN');
688
+ if (xsrf)
689
+ h.set('x-xsrf-token', xsrf);
690
+ return h;
691
+ }
692
+ async function safeCommonFields(jd, page) {
693
+ const u = new URL(page.finalUrl);
694
+ jd.device.configure(jd.cookieStr, page.finalUrl, `${u.protocol}//${u.host}`, page.finalUrl);
695
+ const d = await jd.device.get();
696
+ return [
697
+ ['fp', d.fp],
698
+ ['eid', d.eid],
699
+ ['eid2', d.eid2],
700
+ ['uuid', rand.uuid4()],
701
+ ];
702
+ }
703
+ function safeMethod(page, validateType = '') {
704
+ let methods = page.config.list ?? [];
705
+ if (validateType)
706
+ methods = methods.filter((m) => m && typeof m === 'object' && m.validateType === validateType);
707
+ for (const m of methods) {
708
+ const params = m && typeof m === 'object' ? m.params : null;
709
+ if (params && typeof params === 'object' && params.m && m.enP && m.validateType && SAFE_LOGIN_SMS_TYPES.has(m.validateType))
710
+ return m;
711
+ }
712
+ return {};
713
+ }
714
+ function safeMessage(payload, fallback, secret = '') {
715
+ const r = payload?.resultData;
716
+ let msg = r && typeof r === 'object' ? String(r.msg ?? '') : '';
717
+ msg = msg || String(payload?.resultMsg || fallback);
718
+ return secret ? msg.replaceAll(String(secret), '<masked>') : msg;
719
+ }
720
+ const configValue = (page, key) => String(page.config[key] || page.requestParams[key] || '');
721
+ /** 发额外安全验证的第二条短信(send_safe_mobile_code)。 */
722
+ export async function sendSafeCode(jd, page, validateType = '', delivery = 'msg') {
723
+ const method = safeMethod(page, validateType);
724
+ if (!Object.keys(method).length)
725
+ return { success: false, message: '额外安全验证没有可直接执行的手机号分支', method, payload: {} };
726
+ const mobile = String(method.params.m);
727
+ const keyRes = await jd.send({ url: 'https://aq.jd.com/pwd/gmpk', headers: safeHeaders(jd, page).get(), query: [['s', '1']], timeout: 20 });
728
+ const keyPayload = await parseResponse(keyRes);
729
+ const pub = String(keyPayload.resultData ?? '');
730
+ if (keyRes.status !== 200 || keyPayload.resultCode !== '10000' || !pub)
731
+ return { success: false, message: '额外安全验证加密公钥获取失败', method, payload: keyPayload };
732
+ const form = [
733
+ ['p', method.enP],
734
+ ['m', await summerEncrypt(jd.http, pub, mobile)],
735
+ ['v', method.validateType],
736
+ ['o', configValue(page, 'o')],
737
+ ['s', configValue(page, 's')],
738
+ ...(await safeCommonFields(jd, page)),
739
+ ['f', delivery],
740
+ ];
741
+ const res = await jd.send({ method: 'POST', url: 'https://aq.jd.com/mobile/getCode', headers: safeHeaders(jd, page, true).get(), form, timeout: 20 });
742
+ const payload = await parseResponse(res);
743
+ const success = res.status === 200 && payload.success === true;
744
+ return { success, message: safeMessage(payload, success ? '额外安全验证短信已发送' : '额外安全验证短信发送失败', mobile), method, payload };
745
+ }
746
+ /** 校验额外安全验证的短信码并跟随返回页(submit_safe_mobile_code)。 */
747
+ export async function submitSafeCode(jd, page, method, code) {
748
+ if (!/^\d{6}$/.test(String(code ?? '')))
749
+ return { success: false, message: '额外安全验证短信验证码必须是六位数字', payload: {} };
750
+ const form = [
751
+ ['p', method.enP ?? ''],
752
+ ['c', String(code)],
753
+ ['v', method.validateType ?? ''],
754
+ ['o', configValue(page, 'o')],
755
+ ['s', configValue(page, 's')],
756
+ ...(await safeCommonFields(jd, page)),
757
+ ['rnd', pyFloatStr(rand.random())],
758
+ ];
759
+ const res = await jd.send({ method: 'POST', url: 'https://aq.jd.com/mobile/validateCode', headers: safeHeaders(jd, page, true).get(), form, timeout: 20 });
760
+ const payload = await parseResponse(res);
761
+ if (res.status !== 200 || payload.success !== true)
762
+ return { success: false, message: safeMessage(payload, '额外安全验证短信校验失败'), payload };
763
+ const target = payload.resultData && typeof payload.resultData === 'object' ? payload.resultData.page : '';
764
+ if (target) {
765
+ const resolved = new URL(String(target), page.finalUrl);
766
+ if (resolved.protocol !== 'https:' || !resolved.hostname)
767
+ return { success: false, message: '额外安全验证返回地址异常', payload };
768
+ await follow(jd, resolved.href, safeHeaders(jd, page));
769
+ }
770
+ return { success: true, message: '额外安全验证短信校验通过', payload };
771
+ }
772
+ /** 短信图形验证码(JCAP):纯程序求解,返回 graphicCaptchaVerifyToken。 */
773
+ export function solveSmsCaptcha(jd, ctx, mobile) {
774
+ return solveCaptcha(jd, { sessionId: ctx.captchaSessionId, account: mobile, pageUrl: LOGIN_PAGE });
775
+ }