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,132 @@
1
+ import { jsonLoads, quote } from '../../../core/py.js';
2
+ import * as rand from '../../../core/rand.js';
3
+ import { openSocket } from '../../../core/stream.js';
4
+ import { CHAT_ORIGIN, WS_HEADERS } from './profile.js';
5
+ import { generateWid } from './util.js';
6
+ /**
7
+ * 京东咚咚 WebSocket(上游 jd_apis/jd_chat_ws.py 的 JdChatWS):纯 JSON 帧,信封由 packing() 生成,
8
+ * aid 必须在顶层;建连后先心跳再欢迎语;心跳 30 秒一次且不带 id。
9
+ */
10
+ /** 咚咚消息类型(MsgType,取自 bundle 模块 5a50 的常量表)。 */
11
+ export const MsgType = {
12
+ CHAT_MESSAGE: 'chat_message',
13
+ CHAT_MESSAGE_RESULT: 'chat_message_result',
14
+ CHAT_SESSION_OPEN: 'chat_session_open',
15
+ CHAT_SESSION_CLOSE: 'chat_session_close',
16
+ CHAT_CUSTOMER_LEAVE: 'chat_customer_leave',
17
+ CLIENT_HEARTBEAT: 'client_heartbeat',
18
+ EVENT_MESSAGE: 'event_message',
19
+ SYS_MSG: 'sys_msg',
20
+ FAILURE: 'failure',
21
+ AUTH: 'auth',
22
+ AUTH_RESULT: 'auth_result',
23
+ ACK: 'ack',
24
+ REVOKE_MESSAGE: 'revoke_message',
25
+ MSG_READ_ACK: 'msg_read_ack',
26
+ MSG_RECEIVE_ACK: 'msg_receive_ack',
27
+ };
28
+ export const HOSTS = ['ws1-dd.jd.com', 'ws0-dd.jd.com', 'ws3-dd.jd.com'];
29
+ const CUSTOMER_APP = 'im.customer';
30
+ export const WAITER_APP = 'jd.waiter';
31
+ const CLIENT_TYPE = 'comet';
32
+ const VER = '4.2';
33
+ export const HEARTBEAT_INTERVAL = 30_000;
34
+ /** 本地时间 `YYYY-MM-DD HH:MM:SS`(datetime.now().strftime)。 */
35
+ function localDatetime(ms) {
36
+ const d = new Date(ms);
37
+ const p = (v) => String(v).padStart(2, '0');
38
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
39
+ }
40
+ export class ChatClient {
41
+ jd;
42
+ venderId;
43
+ venderApp;
44
+ url;
45
+ socket = null;
46
+ constructor(jd, venderId = '1', venderApp = WAITER_APP, host = HOSTS[0]) {
47
+ this.jd = jd;
48
+ this.venderId = venderId;
49
+ this.venderApp = venderApp;
50
+ const chat = jd.chat;
51
+ const params = [
52
+ ['pin', quote(jd.pin ?? '', '')],
53
+ ['appId', chat.app_id || CUSTOMER_APP],
54
+ ['aid', chat.aid ?? ''],
55
+ ['clientType', chat.client_type || CLIENT_TYPE],
56
+ ['_wid_', generateWid()],
57
+ ];
58
+ this.url = `wss://${host}/?${params.map(([k, v]) => `${k}=${v}`).join('&')}`;
59
+ }
60
+ /** 信封(packing)。 */
61
+ packing(type, body, to, id) {
62
+ const packet = {
63
+ from: { app: CUSTOMER_APP, pin: this.jd.pin ?? '', clientType: CLIENT_TYPE },
64
+ datetime: localDatetime(rand.now()),
65
+ ver: VER,
66
+ lang: 'zh_CN',
67
+ aid: this.jd.chat.aid ?? '',
68
+ type,
69
+ to: to ?? { app: this.venderApp },
70
+ timestamp: rand.now(),
71
+ };
72
+ if (type !== MsgType.CLIENT_HEARTBEAT)
73
+ packet.id = id || rand.uuid4().replaceAll('-', '');
74
+ if (body && Object.keys(body).length)
75
+ packet.body = body;
76
+ return packet;
77
+ }
78
+ chatinfo(pid = '', orderId = '') {
79
+ const info = { venderId: this.venderId, ct: '3', mt: '51' };
80
+ if (pid)
81
+ info.pid = pid;
82
+ if (orderId)
83
+ info.orderId = orderId;
84
+ return info;
85
+ }
86
+ textPacket(text, pid = '', orderId = '') {
87
+ return this.packing(MsgType.CHAT_MESSAGE, { content: text, type: 'text', chatinfo: this.chatinfo(pid, orderId) });
88
+ }
89
+ /** 会话首帧(sendHello):type=config + cfg.welcome。 */
90
+ helloPacket(pid = '', orderId = '') {
91
+ let content = `顾客${this.jd.pin}发起咨询`;
92
+ if (pid)
93
+ content += `(商品编号:${pid})`;
94
+ if (orderId)
95
+ content += `(订单编号:${orderId})`;
96
+ return this.packing(MsgType.CHAT_MESSAGE, {
97
+ content,
98
+ type: 'config',
99
+ chatinfo: this.chatinfo(pid, orderId),
100
+ uniformBizInfo: {},
101
+ action: { code: 'cfg.welcome' },
102
+ });
103
+ }
104
+ /** 心跳发给自己这一侧(to.app = im.customer)。 */
105
+ heartbeatPacket() {
106
+ return this.packing(MsgType.CLIENT_HEARTBEAT, null, { app: CUSTOMER_APP });
107
+ }
108
+ async connect(signal) {
109
+ this.socket = await openSocket(this.url, {
110
+ headers: { ...WS_HEADERS, Cookie: this.jd.cookieStr, Origin: CHAT_ORIGIN },
111
+ signal,
112
+ });
113
+ return this.socket;
114
+ }
115
+ async send(packet) {
116
+ await this.socket.send(JSON.stringify(packet));
117
+ }
118
+ close() {
119
+ this.socket?.close();
120
+ this.socket = null;
121
+ }
122
+ }
123
+ /** 下行帧:单个对象或数组。 */
124
+ export function packets(raw) {
125
+ try {
126
+ const v = jsonLoads(typeof raw === 'string' ? raw : raw.toString('utf8'));
127
+ return Array.isArray(v) ? v : [v];
128
+ }
129
+ catch {
130
+ return [];
131
+ }
132
+ }
@@ -0,0 +1,470 @@
1
+ import { createCipheriv } from 'node:crypto';
2
+ import { SpecificAIError } from '../../../core/errors.js';
3
+ import * as n from '../../../core/normalize.js';
4
+ import { HttpClient, parseJsonp } from '../../../core/http.js';
5
+ import { compactJson, jsonDumps, jsonLoads, quote, unquote } from '../../../core/py.js';
6
+ import * as rand from '../../../core/rand.js';
7
+ import { authError, scope } from '../../../core/toolkit.js';
8
+ import { DEVICE_PROFILE, DeviceToken, H5st } from './js.js';
9
+ import { API_URL, APPID_PC_ITEM, AXIOS_ORDER, BROWSER, CLIENT_WH5, COOKIE_DOMAIN, FETCH_ORDER, H5ST_SIGN_KEYS, ITEM_ORIGIN, ITEM_REFERER, PROFILE, SEARCH_REFERER, xhr, } from './profile.js';
10
+ import { sha256Hex } from './util.js';
11
+ /**
12
+ * 京东 web 端的会话(上游 builder/auth.py 的 JdAuth + jd_apis/jd_api.py 的 call_api)。
13
+ *
14
+ * 上游的 cookie 是一张不分域的有序表,发往所有京东站点,响应里的 Set-Cookie 按名字合并(空值即删除);
15
+ * 这里照搬:凭证里的 cookie 统一记在 `.jd.com` 下,请求时显式带上整张表。
16
+ * 按 origin 保存的 localStorage 放在凭证的 device.local_storage,咚咚会话参数放在 extra.chat。
17
+ */
18
+ /** SimpleCookie 认得的属性(其余 key=value 会被它当作新 cookie)。 */
19
+ const RESERVED = new Set(['expires', 'path', 'comment', 'domain', 'max-age', 'secure', 'httponly', 'version', 'samesite']);
20
+ const FLAGS = new Set(['secure', 'httponly']);
21
+ /** 合法的 cookie 名(签名脚本交回的 cookie 按它过滤)。 */
22
+ const COOKIE_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
23
+ /** `http.cookies.SimpleCookie().load(line)` 的结果:解析失败时为空。 */
24
+ export function simpleCookie(line) {
25
+ const patt = /\s*(?<key>[\w\d!#%&'~_`><@,:/$*+\-.^|)(?}{=]+?)(\s*=\s*(?<val>"(?:[^\\"]|\\.)*"|\w{3},\s[\w\d\s-]{9,11}\s[\d:]{8}\sGMT|[\w\d!#%&'~_`><@,:/$*+\-.^|)(?}{=\[\]]*))?\s*(\s+|;|$)/y;
26
+ const items = [];
27
+ let seen = false;
28
+ let i = 0;
29
+ while (i < line.length) {
30
+ patt.lastIndex = i;
31
+ const m = patt.exec(line);
32
+ if (!m || m[0].length === 0)
33
+ break;
34
+ i = patt.lastIndex;
35
+ const key = m.groups.key;
36
+ const value = m.groups.val;
37
+ if (key[0] === '$')
38
+ continue;
39
+ const lower = key.toLowerCase();
40
+ if (RESERVED.has(lower)) {
41
+ if (!seen)
42
+ return [];
43
+ if (value == null && !FLAGS.has(lower))
44
+ return [];
45
+ }
46
+ else if (value != null) {
47
+ items.push([key, cookieUnquote(value)]);
48
+ seen = true;
49
+ }
50
+ else
51
+ return [];
52
+ }
53
+ return items;
54
+ }
55
+ function cookieUnquote(v) {
56
+ if (v.length < 2 || v[0] !== '"' || v[v.length - 1] !== '"')
57
+ return v;
58
+ return v.slice(1, -1).replace(/\\(?:([0-3][0-7][0-7])|(.))/g, (_, oct, ch) => (oct ? String.fromCharCode(parseInt(oct, 8)) : ch));
59
+ }
60
+ export class Jd {
61
+ ctx;
62
+ http;
63
+ h5st;
64
+ device;
65
+ /** 与凭证同步的有序 cookie 表。 */
66
+ cookies;
67
+ /** searchWare 记下的关键词,后续同页请求用同一个 Referer。 */
68
+ searchKeyword = '';
69
+ constructor(ctx) {
70
+ this.ctx = ctx;
71
+ this.http = new HttpClient({ browser: BROWSER, os: 'windows', proxy: ctx.config.proxy, timeout: ctx.config.timeout, log: ctx.log });
72
+ this.cookies = new Map(scope(ctx.credential).cookies.map((c) => [c.name, c.value]));
73
+ const host = { http: this.http, log: ctx.log };
74
+ this.h5st = new H5st(host, {
75
+ load: () => {
76
+ const c = ctx.credential.device.h5st;
77
+ return c?.profile === PROFILE.profileId ? c : null;
78
+ },
79
+ save: (cache) => {
80
+ ctx.credential.device.h5st = cache;
81
+ },
82
+ });
83
+ this.device = new DeviceToken(host);
84
+ }
85
+ // ---------------------------------------------------------------- cookie
86
+ get cookieStr() {
87
+ return [...this.cookies].map(([k, v]) => `${k}=${v}`).join('; ');
88
+ }
89
+ cookie(name) {
90
+ return this.cookies.get(name);
91
+ }
92
+ /** 合并 cookie;空值按服务端删除指令处理(update_cookies)。 */
93
+ update(values) {
94
+ const entries = Symbol.iterator in Object(values) ? values : Object.entries(values);
95
+ let changed = false;
96
+ for (const [key, value] of entries) {
97
+ if (value == null || value === '') {
98
+ if (this.cookies.delete(key))
99
+ changed = true;
100
+ }
101
+ else if (this.cookies.get(key) !== String(value)) {
102
+ this.cookies.set(key, String(value));
103
+ changed = true;
104
+ }
105
+ }
106
+ if (changed)
107
+ this.persist();
108
+ }
109
+ persist() {
110
+ const old = new Map(scope(this.ctx.credential).cookies.map((c) => [c.name, c]));
111
+ const list = [];
112
+ for (const [name, value] of this.cookies) {
113
+ const prev = old.get(name);
114
+ list.push({ name, value, domain: COOKIE_DOMAIN, path: '/', expires: prev?.value === value ? (prev.expires ?? null) : null });
115
+ }
116
+ scope(this.ctx.credential).cookies.splice(0, Infinity, ...list);
117
+ }
118
+ /** 吸收响应的 Set-Cookie 与 `x-rp-sdtoken`(absorb_response + _absorb_response)。 */
119
+ absorb(res) {
120
+ const lines = res.headers.getSetCookie();
121
+ for (const line of lines)
122
+ this.update(simpleCookie(line));
123
+ const jar = new Map();
124
+ for (const line of lines) {
125
+ const i = line.indexOf('=');
126
+ if (i < 0)
127
+ continue;
128
+ jar.set(line.slice(0, i).trim(), line.slice(i + 1).split(';', 1)[0].trim());
129
+ }
130
+ this.update([...jar].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
131
+ const raw = res.headers.get('x-rp-sdtoken');
132
+ if (raw?.startsWith('set;')) {
133
+ const parts = splitN(raw, ';', 2);
134
+ if (parts.length === 3)
135
+ this.update([['sdtoken', parts[2]]]);
136
+ }
137
+ }
138
+ /** 登录态(thor 或 pt_key,加 pin)。 */
139
+ get pin() {
140
+ const raw = this.cookie('pin') || this.cookie('pt_pin') || this.cookie('_pst');
141
+ return raw ? unquote(raw) : null;
142
+ }
143
+ get isLogin() {
144
+ return Boolean((this.cookie('thor') || this.cookie('pt_key')) && this.pin);
145
+ }
146
+ // ---------------------------------------------------------------- localStorage 与咚咚会话
147
+ get storage() {
148
+ return (this.ctx.credential.device.local_storage ??= {});
149
+ }
150
+ static storageOrigin(pageUrl) {
151
+ try {
152
+ const u = new URL(pageUrl);
153
+ if (u.protocol && u.host)
154
+ return `${u.protocol}//${u.host}`.toLowerCase();
155
+ }
156
+ catch { }
157
+ return String(pageUrl ?? '');
158
+ }
159
+ localStorageFor(pageUrl) {
160
+ return { ...(this.storage[Jd.storageOrigin(pageUrl)] ?? {}) };
161
+ }
162
+ replaceLocalStorage(pageUrl, values) {
163
+ const origin = Jd.storageOrigin(pageUrl);
164
+ if (!origin || !values || typeof values !== 'object')
165
+ return;
166
+ this.storage[origin] = Object.fromEntries(Object.entries(values).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)]));
167
+ }
168
+ get chat() {
169
+ return (this.ctx.credential.extra.chat ??= { aid: null, app_id: 'im.customer', dvc: null, client_type: 'comet' });
170
+ }
171
+ setChatInfo(values) {
172
+ for (const [k, v] of Object.entries(values))
173
+ if (v)
174
+ this.chat[k] = v;
175
+ }
176
+ get deviceProfile() {
177
+ return this.ctx.credential.device.profile ?? null;
178
+ }
179
+ setDeviceProfile(value) {
180
+ if (value)
181
+ this.ctx.credential.device.profile = value;
182
+ }
183
+ /** 签名脚本(WebM / JCAP 的 run.js)写的 cookie 与 localStorage 交回会话。 */
184
+ absorbScript(pageUrl, result) {
185
+ if (result.cookies && typeof result.cookies === 'object') {
186
+ this.update(Object.entries(result.cookies).filter(([k]) => COOKIE_NAME.test(k)).map(([k, v]) => [k, String(v ?? '')]));
187
+ }
188
+ if (result.localStorage && typeof result.localStorage === 'object')
189
+ this.replaceLocalStorage(pageUrl, result.localStorage);
190
+ }
191
+ // ---------------------------------------------------------------- 设备票据
192
+ /** 用 pc-tk.js 换设备票据写进 cookie;画像版本变化时强制换新。 */
193
+ async refreshDevice(page, origin, referer) {
194
+ this.device.configure(this.cookieStr, page, origin, referer);
195
+ const force = this.deviceProfile !== DEVICE_PROFILE;
196
+ const d = await this.device.get(force);
197
+ this.update([
198
+ ['3AB9D23F7A4B3C9B', d.eid],
199
+ ['3AB9D23F7A4B3CSS', d.eid2],
200
+ ...(d.giaD ? [['_gia_d', d.giaD]] : []),
201
+ ]);
202
+ this.setDeviceProfile(DEVICE_PROFILE);
203
+ return d;
204
+ }
205
+ // ---------------------------------------------------------------- 请求
206
+ /** 发请求并吸收 cookie。默认显式带整张 cookie 表。 */
207
+ async send(req) {
208
+ const res = await this.http.request({ cookies: Object.fromEntries(this.cookies), ...req });
209
+ this.absorb(res);
210
+ return res;
211
+ }
212
+ /** 签 h5st(builder/params.py with_h5st):body 先取 SHA-256。 */
213
+ async signH5st(params, appId) {
214
+ const sign = {};
215
+ for (const k of H5ST_SIGN_KEYS)
216
+ if (params.has(k))
217
+ sign[k] = params.get(k);
218
+ if ('body' in sign)
219
+ sign.body = sha256Hex(String(sign.body));
220
+ return String((await this.h5st.sign(sign, appId)).h5st ?? '');
221
+ }
222
+ /**
223
+ * api.m.jd.com 通用调用(JdAPI.call_api):装配 query → 算 h5st → 发请求;403 空 body 时重签重试。
224
+ * 返回平台原始 JSON(或 jsonp 解开后的对象);风控处置(x-rp-content)命中时照样返回原始结果,
225
+ * 解开的处置挂在不可枚举的 `_disposal` 上,由 checkRisk 报 RISK_CONTROL。
226
+ */
227
+ async call(functionId, o = {}) {
228
+ const method = o.method ?? 'POST';
229
+ const sign = o.sign ?? true;
230
+ const withTime = o.withTime ?? true;
231
+ const retry = o.retry ?? 2;
232
+ const params = new Map();
233
+ for (const [k, v] of Object.entries(o.client ?? CLIENT_WH5))
234
+ if (!params.has(k))
235
+ params.set(k, v);
236
+ params.set('functionId', functionId);
237
+ if (o.withUuid ?? true) {
238
+ const uuid = o.uuid ?? this.cookie('__jdu');
239
+ if (uuid)
240
+ params.set('uuid', uuid);
241
+ }
242
+ if (o.body !== undefined)
243
+ params.set('body', typeof o.body === 'string' ? o.body : compactJson(o.body));
244
+ if (o.jsonp)
245
+ params.set('jsonp', o.jsonp);
246
+ for (const [k, v] of o.extra ?? [])
247
+ params.set(k, v);
248
+ if (sign)
249
+ this.h5st.configure(this.cookieStr, o.origin || ITEM_ORIGIN, o.referer || ITEM_REFERER);
250
+ const eid = this.cookie('3AB9D23F7A4B3CSS');
251
+ if (eid)
252
+ params.set('x-api-eid-token', eid);
253
+ for (const k of o.drop ?? [])
254
+ params.delete(k);
255
+ const axios = o.axios ?? Boolean(o.rpClient);
256
+ let h;
257
+ if (o.jsonp)
258
+ throw new Error('jsonp 调用未移植');
259
+ else if (method !== 'POST' || o.bodyInQuery)
260
+ h = xhr({ axios, accept: o.accept });
261
+ else
262
+ h = xhr({ axios, form: true, accept: o.accept });
263
+ if (o.contentType != null)
264
+ h.set('content-type', o.contentType);
265
+ h.referer(o.referer || ITEM_REFERER);
266
+ if (o.origin)
267
+ h.origin(o.origin);
268
+ if (o.rpClient) {
269
+ h.set('x-referer-page', o.refererPage || (o.origin ?? '') + '/');
270
+ h.set('x-rp-client', o.rpClient);
271
+ }
272
+ h.reorder(axios ? AXIOS_ORDER : FETCH_ORDER);
273
+ const url = `${API_URL}${o.path ?? '/client.action'}`;
274
+ let current = params;
275
+ for (let attempt = 0; attempt <= retry; attempt++) {
276
+ if (withTime)
277
+ current.set('t', String(rand.now()));
278
+ if (sign)
279
+ current.set('h5st', await this.signH5st(current, o.appId ?? APPID_PC_ITEM));
280
+ if (o.order)
281
+ current = reorder(current, o.order);
282
+ let query = [...current];
283
+ if (o.appendTime)
284
+ query.push(['t', String(rand.now())]);
285
+ let res;
286
+ if (method === 'POST') {
287
+ let formBody = '';
288
+ if (!o.bodyInQuery) {
289
+ formBody = String(query.find(([k]) => k === 'body')?.[1] ?? '');
290
+ query = query.filter(([k]) => k !== 'body');
291
+ }
292
+ res = await this.send({ method: 'POST', url, headers: h.get(), query, form: o.bodyInQuery ? undefined : [['body', formBody]] });
293
+ }
294
+ else {
295
+ res = await this.send({ method: 'GET', url, headers: h.get(), query });
296
+ }
297
+ const raw = new Uint8Array(await res.clone().arrayBuffer());
298
+ const rejected = res.status === 403 && raw.length === 0;
299
+ const risk = readDisposal(res);
300
+ if (risk) {
301
+ this.ctx.log.warn(`${functionId} 被风控拦下:${risk}`);
302
+ return withDisposal(await parse(res), res.headers.get('x-rp-content'));
303
+ }
304
+ if (!rejected || attempt === retry) {
305
+ if (rejected)
306
+ this.ctx.log.warn(`${functionId} 连续 ${retry + 1} 次 403 空 body:被限流、风控或登录态失效`);
307
+ return parse(res);
308
+ }
309
+ await rand.sleep(600 * (attempt + 1));
310
+ }
311
+ }
312
+ /** risk_h5 信封(_risk_post):内层 JSON 经 AES-CBC 加密。 */
313
+ async riskPost(referer, functionId, inner) {
314
+ const raw = Buffer.from(jsonDumps(inner, { separators: [',', ':'], ensureAscii: false }), 'utf8');
315
+ const cipher = createCipheriv('aes-128-cbc', Buffer.from('rhiasnkdhandrisk'), Buffer.from('r-s-h-n_r_isnkdk'));
316
+ const encrypted = Buffer.concat([cipher.update(raw), cipher.final()]);
317
+ const outer = { sdkClient: 'pc', sdkVersion: 'pc_2.1.0', enbody: encrypted.toString('base64url') };
318
+ const form = [
319
+ ['appid', 'risk_h5'],
320
+ ['functionId', functionId],
321
+ ['body', jsonDumps(outer, { separators: [',', ':'], ensureAscii: false })],
322
+ ];
323
+ const eid = this.cookie('3AB9D23F7A4B3CSS');
324
+ if (eid)
325
+ form.push(['x-api-eid-token', eid]);
326
+ const h = xhr({ form: true, accept: 'application/json' }).referer(referer).origin('https://cfe.m.jd.com');
327
+ const res = await this.send({ method: 'POST', url: `${API_URL}/api`, headers: h.get(), form });
328
+ return res.json();
329
+ }
330
+ /** 写操作 / 需要登录的接口前检查登录态。 */
331
+ requireLogin() {
332
+ if (!this.isLogin)
333
+ throw authError(this.ctx, '当前账号缺少 thor / pin,请重新登录');
334
+ }
335
+ }
336
+ /** 按浏览器实抓的顺序重排;不在 order 里的键按原顺序垫在后面。 */
337
+ function reorder(params, order) {
338
+ const out = new Map();
339
+ for (const k of order)
340
+ if (params.has(k))
341
+ out.set(k, params.get(k));
342
+ for (const [k, v] of params)
343
+ if (!out.has(k))
344
+ out.set(k, v);
345
+ return out;
346
+ }
347
+ function splitN(s, sep, n) {
348
+ const out = [];
349
+ let rest = s;
350
+ while (out.length < n) {
351
+ const i = rest.indexOf(sep);
352
+ if (i < 0)
353
+ break;
354
+ out.push(rest.slice(0, i));
355
+ rest = rest.slice(i + sep.length);
356
+ }
357
+ out.push(rest);
358
+ return out;
359
+ }
360
+ /** 响应体:JSON,其次 jsonp,都不是时给出状态与片段(_parse)。 */
361
+ export async function parse(res) {
362
+ const text = await res.text();
363
+ try {
364
+ return jsonLoads(text);
365
+ }
366
+ catch {
367
+ const p = parseJsonp(text);
368
+ if (p != null)
369
+ return p;
370
+ return { _status: res.status, _raw: text.slice(0, 1000) };
371
+ }
372
+ }
373
+ /** 解 `x-rp-content` 头里的风控处置(_read_disposal)。 */
374
+ export function decodeDisposal(raw) {
375
+ if (!raw)
376
+ return null;
377
+ for (let cut = 0; cut < 4; cut++) {
378
+ const chunk = cut ? raw.slice(0, raw.length - cut) : raw;
379
+ try {
380
+ const b = Buffer.from(chunk.replaceAll('-', '+').replaceAll('_', '/'), 'base64');
381
+ return jsonLoads(b.toString('utf8'));
382
+ }
383
+ catch { }
384
+ }
385
+ return undefined;
386
+ }
387
+ function readDisposal(res) {
388
+ const raw = res.headers.get('x-rp-content');
389
+ if (!raw)
390
+ return null;
391
+ const info = decodeDisposal(raw);
392
+ if (info === undefined)
393
+ return `x-rp-content 解不开:${raw.slice(0, 60)}`;
394
+ let ev = {};
395
+ try {
396
+ ev = jsonLoads(info?.disposal?.evContent || '{}');
397
+ }
398
+ catch { }
399
+ return `code=${info?.code} ${ev.title ?? ''} ${ev.evTypeTip ?? ''}(接口 ${ev.evApi ?? ''})`;
400
+ }
401
+ /** 搜索页 Referer:searchWare 记下的关键词优先(_search_page_referer)。 */
402
+ export function searchReferer(jd, keyword = '') {
403
+ const kw = keyword || jd.searchKeyword;
404
+ if (kw)
405
+ return `https://search.jd.com/Search?keyword=${quote(kw)}&enc=utf-8`;
406
+ return SEARCH_REFERER;
407
+ }
408
+ /** x-rp-content 头里的处置挂到结果的 `_disposal` 上(不参与序列化,与上游的返回值一致),交给 checkRisk。 */
409
+ function withDisposal(result, header) {
410
+ const out = result && typeof result === 'object' ? result : { _status: 200, _raw: String(result ?? '') };
411
+ Object.defineProperty(out, '_disposal', { value: decodeDisposal(header) ?? { code: null }, enumerable: false, configurable: true });
412
+ return out;
413
+ }
414
+ /**
415
+ * 业务响应里的风控处置 / 登录墙映射成 specific-ai 的错误。
416
+ * 处置有两种来源:body 里的 `disposal` / code 605,以及只在 x-rp-content 响应头里的(body 常为空、状态码 200,见 call)。
417
+ * 403 空 body 要先探测登录态才能分清是登录失效还是限流(见 commands.ts 的 check),这里只处理能直接判定的情况。
418
+ */
419
+ export function checkRisk(jd, res) {
420
+ if (res?.disposal || String(res?.code) === '605') {
421
+ throw new SpecificAIError('RISK_CONTROL', '京东要求人机验证(605),纯程序验证未通过', { detail: { kind: 'captcha', code: res?.code ?? null } });
422
+ }
423
+ const header = res?._disposal;
424
+ if (header) {
425
+ let ev = {};
426
+ try {
427
+ ev = jsonLoads(header.disposal?.evContent || '{}');
428
+ }
429
+ catch { }
430
+ throw new SpecificAIError('RISK_CONTROL', `京东要求人机验证(x-rp-content code=${header.code ?? '?'}):账号或 IP 被风控标记了`, {
431
+ hint: '等风控解除(十几分钟到几小时)后再试',
432
+ detail: { kind: 'captcha', code: header.code ?? null, api: ev.evApi ?? null },
433
+ });
434
+ }
435
+ if (res?._status === 401 || res?.code === 3 || res?.code === '3')
436
+ throw authError(jd.ctx);
437
+ }
438
+ /** 各接口的登录墙:网关的 code 3 之外,购物车回 `pin is null` + passport 地址,浏览历史回 resultCode -100「用户未登录」(2026-09-29 真机)。 */
439
+ function loginWall(res) {
440
+ return (String(res.resultCode) === '-100' ||
441
+ /未登录|pin is null/i.test(String(res.message ?? res.msg ?? res.resultMsg ?? res.echo ?? '')) ||
442
+ /passport\.jd\.com/.test(String(res.url ?? '')));
443
+ }
444
+ /**
445
+ * 业务失败(上游只返回原始 JSON,由调用方自己看)。所有接口都认的:
446
+ * - body 不是 JSON(parse 给出 `_raw`);
447
+ * - 网关错误 `{code, echo}`(如 601、1 Content-Type 不兼容);
448
+ * - `success: false`(购物车、浏览历史的失败响应)。
449
+ * `strict` 时 `code` 存在且不是 0 / "0" 也算失败:只用于核对过正常响应的 code 的接口——
450
+ * hotwords 为 `0`,getChatSessionLog 为 `"0"`(2026-09-29 真机),getAidInfo 为 `"0"`(上游 get_aid_info 的注释)。
451
+ * 其余接口(详情、评价、优惠券、关注、浏览历史、购物车、搜索、relsearch / relwords、queryLastLogs)正常响应里 code 的含义没核对过,
452
+ * 例如 getRecommendCoupon 会回只有 `{"code":"1"}` 的响应,分不清是没有券还是失败,所以不按 code 判。
453
+ */
454
+ export function checkBusiness(jd, res, strict = false) {
455
+ if (res == null || typeof res !== 'object' || Array.isArray(res))
456
+ return;
457
+ if ('_raw' in res) {
458
+ throw new SpecificAIError('UPSTREAM', `京东返回了无法解析的响应(HTTP ${res._status})`, { detail: { status: res._status ?? null, body: String(res._raw).slice(0, 200) } });
459
+ }
460
+ const badCode = res.code != null && String(res.code) !== '0';
461
+ const failed = res.success === false || (badCode && (strict || res.echo != null));
462
+ if (!failed)
463
+ return;
464
+ const message = n.str(res.message ?? res.msg ?? res.resultMsg ?? res.echo);
465
+ if (loginWall(res))
466
+ throw authError(jd.ctx, message ? `登录态已失效:${message}` : undefined);
467
+ throw new SpecificAIError('UPSTREAM', `京东返回业务错误(code=${res.code ?? res.resultCode ?? '-'})${message ? `:${message}` : ''}`, {
468
+ detail: { code: res.code ?? null, result_code: res.resultCode ?? null, echo: res.echo ?? null, message },
469
+ });
470
+ }