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,166 @@
1
+ import * as n from '../../../core/normalize.js';
2
+ import { noWaterImage } from './api.js';
3
+ /**
4
+ * 小红书原始对象 → 归一化类型(AGENTS 6.2)。字段来源以上游 xhs_utils/data_util.py 的 handle_* 为准,
5
+ * 并兼容列表接口里的卡片形态(note_card / user 等)。
6
+ *
7
+ * 小红书的 url 必须带 xsec_token(AGENTS 4.8):拿不到 token 的对象,url 为不带 token 的地址,
8
+ * 取详情前需要从带 token 的列表或分享链接进入。
9
+ */
10
+ const WEB = 'https://www.xiaohongshu.com';
11
+ export function noteUrl(id, token, source = 'pc_feed') {
12
+ return token ? `${WEB}/explore/${id}?xsec_token=${encodeURIComponent(String(token))}&xsec_source=${source}` : `${WEB}/explore/${id}`;
13
+ }
14
+ export function userUrl(id, token, source = 'pc_note') {
15
+ if (id == null || id === '')
16
+ return null;
17
+ return token ? `${WEB}/user/profile/${id}?xsec_token=${encodeURIComponent(String(token))}&xsec_source=${source}` : `${WEB}/user/profile/${id}`;
18
+ }
19
+ export function ref(u) {
20
+ if (!u)
21
+ return null;
22
+ const id = u.user_id ?? u.userId ?? u.id;
23
+ return n.userRef({ id, name: u.nickname ?? u.nick_name ?? u.name ?? u.userName, url: userUrl(id, u.xsec_token) });
24
+ }
25
+ const kindOf = (type) => (type === 'video' ? 'video' : 'image');
26
+ /** 图片的地址:info_list 里取默认档(上游取 info_list[1]),否则 url_default / url。 */
27
+ function imageUrl(img) {
28
+ const list = img?.info_list ?? [];
29
+ return n.url(list.find((x) => x.image_scene === 'WB_DFT')?.url ?? list[1]?.url ?? list[0]?.url ?? img?.url_default ?? img?.url);
30
+ }
31
+ /** 编码的优先顺序:新版 Web 把编码名混淆成 EF4(h264)/ EF5(h265)等,优先兼容性最好的 h264,其余编码排在后面。 */
32
+ const CODECS = ['h264', 'EF4', 'h265', 'EF5'];
33
+ const streamUrl = (s) => s?.master_url || s?.url || s?.backup_urls?.[0] || undefined;
34
+ /** 上游 handle_note_info 的视频地址:按编码优先顺序取第一个有地址的流,否则 origin_video_key 拼 sns-video-bd。 */
35
+ function videoMedia(v) {
36
+ const stream = v?.media?.stream ?? {};
37
+ const codecs = [...CODECS, ...Object.keys(stream).filter((k) => !CODECS.includes(k))];
38
+ const s = codecs.flatMap((c) => (Array.isArray(stream[c]) ? stream[c] : [])).find(streamUrl);
39
+ const url = streamUrl(s) ?? (v?.consumer?.origin_video_key ? `https://sns-video-bd.xhscdn.com/${v.consumer.origin_video_key}` : null);
40
+ if (!url)
41
+ return null;
42
+ return n.media({ id: n.idOrNull(v?.media?.video_id), type: 'video', url: n.url(url), width: s?.width ?? null, height: s?.height ?? null, duration: n.seconds(v?.capa?.duration ?? (s?.duration ? s.duration / 1000 : null)) }, v);
43
+ }
44
+ /** 笔记详情(feed 接口的 items[i]:{id, xsec_token?, note_card})。 */
45
+ export function note(v, token) {
46
+ const c = v.note_card ?? v;
47
+ const id = String(v.id ?? c.note_id);
48
+ const kind = kindOf(c.type);
49
+ const images = (c.image_list ?? []).map((img) => imageUrl(img)).filter(Boolean);
50
+ const media = [];
51
+ if (kind === 'video') {
52
+ const vm = videoMedia(c.video);
53
+ if (vm)
54
+ media.push(vm);
55
+ }
56
+ else
57
+ for (const [i, img] of (c.image_list ?? []).entries()) {
58
+ const url = imageUrl(img);
59
+ if (url)
60
+ media.push(n.media({ id: String(i), type: 'image', url, width: img.width ?? null, height: img.height ?? null }, img));
61
+ }
62
+ const i = c.interact_info ?? {};
63
+ return n.item({
64
+ id,
65
+ kind,
66
+ url: noteUrl(id, v.xsec_token ?? token),
67
+ title: n.str(c.title) ?? n.str(c.display_title),
68
+ text: n.str(c.desc),
69
+ author: ref(c.user),
70
+ created_at: n.time(c.time),
71
+ cover: n.url(c.cover?.url_default ?? c.cover?.url) ?? images[0] ?? null,
72
+ media,
73
+ stats: { likes: n.count(i.liked_count), comments: n.count(i.comment_count), collects: n.count(i.collected_count), shares: n.count(i.share_count) },
74
+ }, v);
75
+ }
76
+ /** 列表卡片:用户笔记 / 点赞 / 收藏({note_id, xsec_token, display_title, cover, user, interact_info, type})与搜索、推荐流({id, xsec_token, note_card})。 */
77
+ export function card(v) {
78
+ const c = v.note_card ?? v;
79
+ const id = String(v.id || v.note_id || c.note_id || '');
80
+ const i = c.interact_info ?? {};
81
+ return n.item({
82
+ id,
83
+ kind: kindOf(c.type),
84
+ url: id ? noteUrl(id, v.xsec_token ?? c.xsec_token) : null,
85
+ title: n.str(c.display_title) ?? n.str(c.title),
86
+ author: ref(c.user),
87
+ created_at: n.time(c.time),
88
+ cover: n.url(c.cover?.url_default ?? c.cover?.url ?? c.cover?.info_list?.[0]?.url),
89
+ stats: { likes: n.count(i.liked_count), comments: n.count(i.comment_count), collects: n.count(i.collected_count), shares: n.count(i.shared_count ?? i.share_count) },
90
+ }, v);
91
+ }
92
+ /** 媒体地址(item media / download):图片换成无水印 JPEG,视频用原始流。 */
93
+ export function mediaOf(item) {
94
+ return item.media.map((m) => (m.type === 'image' && m.url ? { ...m, url: noWaterImage(m.url) } : m));
95
+ }
96
+ /** 用户资料(user/otherinfo:{basic_info, interactions, tags})。 */
97
+ export function profile(v, id, token) {
98
+ const b = v.basic_info ?? {};
99
+ const count = (type) => n.count((v.interactions ?? []).find((x) => x.type === type)?.count);
100
+ return n.user({
101
+ id,
102
+ name: n.str(b.nickname),
103
+ handle: n.str(b.red_id),
104
+ avatar: n.url(b.imageb ?? b.images),
105
+ url: userUrl(id, token),
106
+ bio: n.str(b.desc),
107
+ stats: { following: count('follows'), followers: count('fans'), likes: count('interaction') },
108
+ }, v);
109
+ }
110
+ /** user/me。 */
111
+ export function me(v) {
112
+ return n.user({ id: String(v.user_id), name: n.str(v.nickname), handle: n.str(v.red_id), avatar: n.url(v.images ?? v.imageb), url: userUrl(v.user_id), bio: n.str(v.desc) }, v);
113
+ }
114
+ /** 搜索用户的结果项。 */
115
+ export function searchUser(v) {
116
+ const id = String(v.id ?? v.user_id);
117
+ return n.user({
118
+ id,
119
+ name: n.str(v.name ?? v.nickname),
120
+ handle: n.str(v.red_id),
121
+ avatar: n.url(v.image ?? v.avatar),
122
+ url: userUrl(id, v.xsec_token, 'pc_search'),
123
+ bio: n.str(v.desc),
124
+ stats: { followers: n.count(v.fans), items: n.count(v.note_count) },
125
+ }, v);
126
+ }
127
+ /** 私信页的关注列表(users/following/all 的 follow_user_d_t_o_list[i]:{user_id, nick_name, ...})。 */
128
+ export function followingUser(v) {
129
+ const id = String(v.user_id ?? v.userId ?? v.id);
130
+ return n.user({
131
+ id,
132
+ name: n.str(v.nick_name ?? v.nickname ?? v.name),
133
+ handle: n.str(v.red_id),
134
+ avatar: n.url(v.avatar ?? v.image ?? v.images),
135
+ url: userUrl(id),
136
+ bio: n.str(v.desc),
137
+ }, v);
138
+ }
139
+ /** 评论(comment/page 的 comments[i] 与 sub_comments)。 */
140
+ export function comment(v, itemId, parent = null) {
141
+ return n.comment({
142
+ id: String(v.id),
143
+ item_id: itemId,
144
+ parent_id: parent ?? n.idOrNull(v.target_comment?.id) ?? null,
145
+ author: ref(v.user_info),
146
+ text: String(v.content ?? ''),
147
+ created_at: n.time(v.create_time),
148
+ stats: { likes: n.count(v.like_count), replies: n.count(v.sub_comment_count) },
149
+ }, v);
150
+ }
151
+ /** 创作者中心「笔记管理」的笔记(审核状态:0 已发布;1/2 审核中;3 未通过;私密看 permission)。 */
152
+ export function postedNote(v) {
153
+ const id = String(v.id ?? v.note_id);
154
+ const audit = Number(v.audit_status ?? v.status ?? 0);
155
+ const status = v.permission_code === 1 || v.permission_msg === '仅自己可见' ? 'private' : audit === 3 || audit === 4 ? 'rejected' : audit === 1 || audit === 2 ? 'reviewing' : 'published';
156
+ return n.item({
157
+ id,
158
+ kind: v.type === 'video' ? 'video' : 'image',
159
+ url: noteUrl(id, v.xsec_token),
160
+ title: n.str(v.display_title ?? v.title),
161
+ created_at: n.time(v.time ?? v.post_time),
162
+ cover: n.url(v.images_list?.[0]?.url ?? v.cover?.url),
163
+ stats: { views: n.count(v.view_count), likes: n.count(v.likes), comments: n.count(v.comments_count), collects: n.count(v.collected_count), shares: n.count(v.shared_count) },
164
+ status,
165
+ }, v);
166
+ }
@@ -0,0 +1,127 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { CREATOR_JS, PC_JS } from './js.js';
3
+ /**
4
+ * 小红书 web 端的常量:站点、设备画像(上游 xhs_pc/js/reference_profile.json、xhs_creator/js/reference_profile.json)
5
+ * 与浏览器实抓的请求头顺序(上游 xhs_pc/params.py、xhs_creator/params.py)。
6
+ */
7
+ export const PC_REFERENCE = JSON.parse(readFileSync(PC_JS('reference_profile.json'), 'utf8'));
8
+ export const CREATOR_REFERENCE = JSON.parse(readFileSync(CREATOR_JS('reference_profile.json'), 'utf8'));
9
+ export const WEB = 'https://www.xiaohongshu.com';
10
+ export const EDITH = 'https://edith.xiaohongshu.com';
11
+ export const SO = 'https://so.xiaohongshu.com';
12
+ export const LIVE = 'https://live-room.xiaohongshu.com';
13
+ export const AS = 'https://as.xiaohongshu.com';
14
+ export const SEM = 'https://pages.xiaohongshu.com';
15
+ export const CREATOR = 'https://creator.xiaohongshu.com';
16
+ export const CUSTOMER = 'https://customer.xiaohongshu.com';
17
+ export const ROS_UPLOAD = 'https://ros-upload.xiaohongshu.com';
18
+ export const PUSH_URL = 'wss://apppush-rws.xiaohongshu.com/rwp';
19
+ export const COOKIE_DOMAIN = '.xiaohongshu.com';
20
+ /** 上游 curl_cffi 用 chrome146(PC)/ chrome150(Creator)的 TLS 指纹;wreq-js 最新到 chrome_149。 */
21
+ export const BROWSER = 'chrome_146';
22
+ export const CREATOR_BROWSER = 'chrome_149';
23
+ export const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36';
24
+ export const SEC_CH_UA = '"Not;A=Brand";v="8", "Chromium";v="152", "Google Chrome";v="152"';
25
+ export const LOGIN_LANG = 'zh-CN,zh;q=0.9';
26
+ export const BUSINESS_LANG = 'zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7,ja;q=0.6';
27
+ export const ACCEPT_ENCODING = 'gzip, deflate, br, zstd';
28
+ export const XHR_ACCEPT = 'application/json, text/plain, */*';
29
+ export const DOC_ACCEPT = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7';
30
+ // ---------------------------------------------------------------- 头顺序(PC)
31
+ const o = (s) => s.split(' ');
32
+ export const PC_ORDER = {
33
+ navigation: o('upgrade-insecure-requests user-agent sec-ch-ua sec-ch-ua-mobile sec-ch-ua-platform accept accept-encoding accept-language cookie priority sec-fetch-dest sec-fetch-mode sec-fetch-site sec-fetch-user'),
34
+ honeypot: o('sec-ch-ua-platform referer user-agent accept sec-ch-ua content-type sec-ch-ua-mobile accept-encoding accept-language cookie origin priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
35
+ security: o('sec-ch-ua-platform referer sec-ch-ua sec-ch-ua-mobile x-t x-s-common user-agent accept content-type x-s accept-encoding accept-language cookie origin priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
36
+ sem: o('sec-ch-ua-platform referer sec-ch-ua sec-ch-ua-mobile x-t x-s-common user-agent accept x-s accept-encoding accept-language origin priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
37
+ signedPost: o('sec-ch-ua-platform referer sec-ch-ua x-xray-traceid sec-ch-ua-mobile x-t x-b3-traceid x-s-common user-agent accept content-type x-s accept-encoding accept-language cookie origin priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
38
+ signedGet: o('sec-ch-ua-platform referer sec-ch-ua x-xray-traceid sec-ch-ua-mobile x-t x-b3-traceid x-s-common user-agent accept x-s accept-encoding accept-language cookie origin priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
39
+ businessPost: o('referer x-xray-traceid x-t x-b3-traceid x-s-common user-agent accept content-type x-s accept-encoding accept-language cookie origin priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
40
+ businessGet: o('referer x-xray-traceid x-t x-b3-traceid x-s-common user-agent accept x-s accept-encoding accept-language cookie origin priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
41
+ cdeviceGet: o('referer x-xray-traceid c_device_id x-t x-b3-traceid x-s-common user-agent accept x-s accept-encoding accept-language cookie origin priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
42
+ livePost: o('referer xy-common-params x-t x-s-common x-ratelimit-meta accept content-type x-s user-agent accept-encoding accept-language cookie origin priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
43
+ liveGet: o('referer xy-common-params x-t x-s-common x-ratelimit-meta accept x-s user-agent accept-encoding accept-language cookie origin priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
44
+ rapPost: o('referer x-xray-traceid x-t x-b3-traceid x-s-common x-rap-param accept content-type x-s user-agent accept-encoding accept-language cookie origin priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
45
+ xyRapPost: o('xy-direction referer x-xray-traceid x-t x-b3-traceid x-s-common x-rap-param accept content-type x-s user-agent accept-encoding accept-language cookie origin priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
46
+ rapGet: o('referer x-xray-traceid x-t x-b3-traceid x-s-common x-rap-param user-agent accept x-s accept-encoding accept-language cookie origin priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
47
+ };
48
+ // ---------------------------------------------------------------- 头顺序(Creator)
49
+ export const CREATOR_ORDER = {
50
+ navigation: PC_ORDER.navigation,
51
+ honeypot: o('referer user-agent accept content-type accept-encoding accept-language cookie origin priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
52
+ redcaptcha: o('referer x-xray-traceid x-t x-b3-traceid x-s-common user-agent accept content-type x-s accept-encoding accept-language cookie origin priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
53
+ security: o('authorization referer x-t x-s-common user-agent accept content-type x-s accept-encoding accept-language cookie origin priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
54
+ get: o('authorization referer x-xray-traceid x-t x-b3-traceid x-s-common user-agent accept x-s accept-encoding accept-language cookie priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
55
+ post: o('authorization referer x-xray-traceid x-t x-b3-traceid x-s-common user-agent accept content-type x-s accept-encoding accept-language cookie origin priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
56
+ casGet: o('authorization referer x-t x-s-common user-agent accept x-ratelimit-meta x-s accept-encoding accept-language cookie origin priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
57
+ casPost: o('authorization referer x-t x-s-common user-agent accept x-ratelimit-meta content-type x-s accept-encoding accept-language cookie origin priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
58
+ casGetNoRate: o('authorization referer x-t x-s-common user-agent accept x-s accept-encoding accept-language cookie origin priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
59
+ casPostNoRate: o('authorization referer x-t x-s-common user-agent accept content-type x-s accept-encoding accept-language cookie origin priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
60
+ loginUserInfo: o('authorization referer x-xray-traceid x-t x-b3-traceid x-s-common user-agent accept content-type x-s accept-encoding accept-language cookie priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
61
+ noteManager: o('authorization referer x-xray-traceid x-t x-b3-traceid x-s-common user-agent accept x-s accept-encoding accept-language cookie priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
62
+ noteManagerUserInfo: o('authorization referer x-xray-traceid x-t x-b3-traceid x-s-common user-agent accept x-s accept-encoding accept-language cache-control cookie pragma priority sec-fetch-dest sec-fetch-mode sec-fetch-site'),
63
+ };
64
+ /**
65
+ * 上游 xhs_core/http.ordered_wire_headers:键转小写、补 accept-encoding 与 cookie,按实抓顺序排列;
66
+ * 顺序表外的键(optional 除外)直接报错,保证与浏览器的头集合一致。
67
+ */
68
+ export function orderedHeaders(headers, order, cookies, options = {}) {
69
+ const values = {};
70
+ for (const [k, v] of Object.entries(headers))
71
+ if (k.toLowerCase() !== 'authority')
72
+ values[k.toLowerCase()] = v == null ? null : String(v);
73
+ values['accept-encoding'] ??= ACCEPT_ENCODING;
74
+ const cookie = cookieHeader(cookies);
75
+ if (cookie)
76
+ values.cookie = cookie;
77
+ else
78
+ delete values.cookie;
79
+ const optional = new Set(options.optional ?? []);
80
+ const missing = order.filter((k) => !(k in values) && !optional.has(k));
81
+ const unexpected = Object.keys(values).filter((k) => !order.includes(k) && !optional.has(k));
82
+ if (missing.length || ((options.strict ?? true) && unexpected.length)) {
83
+ throw new Error(`请求头与实抓顺序不符:missing=${missing.join(',')} unexpected=${unexpected.join(',')}`);
84
+ }
85
+ const out = order.filter((k) => k in values).map((k) => [k, values[k]]);
86
+ for (const k of Object.keys(values))
87
+ if (optional.has(k) && !order.includes(k))
88
+ out.push([k, values[k]]);
89
+ return out.filter((p) => p[1] != null);
90
+ }
91
+ export function cookieHeader(cookies) {
92
+ return cookies ? Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ') : '';
93
+ }
94
+ /** 导航请求的公共头(上游 get_common_headers)。 */
95
+ export function navigationHeaders(site = 'none') {
96
+ return {
97
+ 'upgrade-insecure-requests': '1',
98
+ 'user-agent': UA,
99
+ 'sec-ch-ua': SEC_CH_UA,
100
+ 'sec-ch-ua-mobile': '?0',
101
+ 'sec-ch-ua-platform': '"Windows"',
102
+ accept: DOC_ACCEPT,
103
+ 'accept-language': LOGIN_LANG,
104
+ priority: 'u=0, i',
105
+ 'sec-fetch-dest': 'document',
106
+ 'sec-fetch-mode': 'navigate',
107
+ 'sec-fetch-site': site,
108
+ 'sec-fetch-user': '?1',
109
+ };
110
+ }
111
+ /** 在 after 之后插入一个头(上游 _insert_header_after / _insert_after)。 */
112
+ export function insertAfter(headers, key, value, after) {
113
+ const out = {};
114
+ let inserted = false;
115
+ for (const [k, v] of Object.entries(headers)) {
116
+ if (k.toLowerCase() === key.toLowerCase())
117
+ continue;
118
+ out[k] = v;
119
+ if (k.toLowerCase() === after.toLowerCase()) {
120
+ out[key] = value;
121
+ inserted = true;
122
+ }
123
+ }
124
+ if (!inserted)
125
+ out[key] = value;
126
+ return out;
127
+ }
@@ -0,0 +1,361 @@
1
+ import { constants, createPublicKey, publicEncrypt } from 'node:crypto';
2
+ import { SpecificAIError } from '../../../core/errors.js';
3
+ import { compactJson, jsonLoads } from '../../../core/py.js';
4
+ import * as rand from '../../../core/rand.js';
5
+ import { openSocket } from '../../../core/stream.js';
6
+ import { celestialLt } from './api.js';
7
+ import { PUSH_URL, UA, WEB } from './profile.js';
8
+ /**
9
+ * 私信 / 直播间的 RWP 长连(上游 apis/xhs_live.py 的 XHSWebSocket 与 IM protobuf 编解码)。
10
+ * 帧是 JSON 信封 `{v, t, m, b}`;私信正文是 protobuf(ChatOneMessage),直播间事件是 base64 的 JSON。
11
+ */
12
+ // ---------------------------------------------------------------- protobuf(上游 _pb_*,只用到 varint / length-delimited)
13
+ function varint(v) {
14
+ let n = BigInt(v);
15
+ if (n < 0n)
16
+ throw new Error('protobuf varint 不能为负');
17
+ const out = [];
18
+ while (n > 0x7fn) {
19
+ out.push(Number(n & 0x7fn) | 0x80);
20
+ n >>= 7n;
21
+ }
22
+ out.push(Number(n));
23
+ return out;
24
+ }
25
+ const field = (num, wire, payload) => [...varint((num << 3) | wire), ...payload];
26
+ const pbUint = (num, v) => field(num, 0, varint(v));
27
+ const pbBool = (num, v) => field(num, 0, varint(v ? 1 : 0));
28
+ const pbBytes = (num, b) => field(num, 2, [...varint(b.length), ...b]);
29
+ const pbString = (num, s) => pbBytes(num, [...Buffer.from(s, 'utf8')]);
30
+ function readVarint(d, o) {
31
+ let v = 0n;
32
+ let shift = 0n;
33
+ while (o < d.length) {
34
+ const b = d[o++];
35
+ v |= BigInt(b & 0x7f) << shift;
36
+ if (!(b & 0x80))
37
+ return [v, o];
38
+ shift += 7n;
39
+ if (shift >= 64n)
40
+ throw new Error('protobuf varint 无效');
41
+ }
42
+ throw new Error('protobuf varint 被截断');
43
+ }
44
+ function readFields(d) {
45
+ const out = new Map();
46
+ let o = 0;
47
+ while (o < d.length) {
48
+ const [tag, o1] = readVarint(d, o);
49
+ o = o1;
50
+ const num = Number(tag >> 3n);
51
+ const wire = Number(tag & 7n);
52
+ let v;
53
+ if (wire === 0)
54
+ [v, o] = readVarint(d, o);
55
+ else if (wire === 2) {
56
+ const [len, o2] = readVarint(d, o);
57
+ v = d.subarray(o2, o2 + Number(len));
58
+ o = o2 + Number(len);
59
+ }
60
+ else
61
+ throw new Error(`protobuf wire type ${wire} 不支持`);
62
+ const list = out.get(num);
63
+ if (list)
64
+ list.push(v);
65
+ else
66
+ out.set(num, [v]);
67
+ }
68
+ return out;
69
+ }
70
+ const last = (f, n) => f.get(n)?.at(-1);
71
+ const text = (f, n) => {
72
+ const v = last(f, n);
73
+ return v instanceof Uint8Array ? Buffer.from(v).toString('utf8') : '';
74
+ };
75
+ const int = (f, n) => {
76
+ const v = last(f, n);
77
+ return typeof v === 'bigint' ? Number(v) : 0;
78
+ };
79
+ /**
80
+ * encode_im_chat_message:ChatSendMessage 包进 ChatOneMessage(type=1, field 9)。
81
+ * 上游的 _pb_* 不省略默认值:token / nickname / ref_id 为空串、group_chat 为 false 也照写;command 为 None 时不写。
82
+ */
83
+ export function encodeChatMessage(m) {
84
+ const send = [
85
+ ...pbString(1, m.mid),
86
+ ...pbUint(2, m.ts),
87
+ ...pbString(3, ''),
88
+ ...pbString(4, m.sender),
89
+ ...pbString(5, m.receiver),
90
+ ...pbString(6, m.content),
91
+ ...pbUint(7, m.contentType),
92
+ ...pbString(8, ''),
93
+ ...pbBool(9, false),
94
+ ...pbString(11, ''),
95
+ ...pbUint(12, 1),
96
+ ];
97
+ return Uint8Array.from([...pbUint(1, 1), ...pbBytes(9, send)]);
98
+ }
99
+ /** decode_im_one_message:field 4 是收到的私信,field 5 是发送回执。 */
100
+ export function decodeChat(data) {
101
+ const outer = readFields(data);
102
+ const out = {};
103
+ const m = last(outer, 4);
104
+ if (m instanceof Uint8Array) {
105
+ const f = readFields(m);
106
+ const payload = text(f, 5);
107
+ let json = null;
108
+ try {
109
+ json = jsonLoads(payload);
110
+ }
111
+ catch { }
112
+ out.message = { mid: text(f, 1), messageId: text(f, 2), ts: int(f, 3), payload, json };
113
+ }
114
+ const a = last(outer, 5);
115
+ if (a instanceof Uint8Array) {
116
+ const f = readFields(a);
117
+ out.ack = { mid: text(f, 1), messageId: text(f, 2), ts: int(f, 3), code: int(f, 5), msg: text(f, 6) };
118
+ }
119
+ return out;
120
+ }
121
+ /** 私信正文常嵌套多层 JSON(上游 demo._im_text):剥出最内层文本。 */
122
+ export function innerText(body) {
123
+ let content = body;
124
+ for (let i = 0; i < 6; i++) {
125
+ if (content && typeof content === 'object' && 'content' in content) {
126
+ content = content.content;
127
+ continue;
128
+ }
129
+ if (typeof content === 'string') {
130
+ try {
131
+ const parsed = jsonLoads(content);
132
+ if (parsed && typeof parsed === 'object') {
133
+ content = parsed;
134
+ continue;
135
+ }
136
+ }
137
+ catch { }
138
+ }
139
+ break;
140
+ }
141
+ return typeof content === 'string' ? content : JSON.stringify(content);
142
+ }
143
+ // ---------------------------------------------------------------- RSA 短链(HTTP 兜底发私信)
144
+ const RSA_N = 'd321555d67813eace010dc27e72ab14876a9b671c7d58d3c9c2064cd60f7e9f79ad3799657b35a1b7654d82725408a71549d5ade11e74bbf1ec39b549ed32116affd4f6b03f2c9c44d91f84157b159a8a225150916e2716cc82dc8fd62385e5a01c83b784c139462a1dd45d47d96ebb4f5068c42b3de8590123a03565a9e5aed';
145
+ /** rsa_encrypt_short_link_payload:RSAES-PKCS1-v1_5 按 117 字节分块,结果按 latin-1 转成字符串。 */
146
+ export function rsaShortLink(payload) {
147
+ const pub = createPublicKey({ key: { kty: 'RSA', n: Buffer.from(RSA_N, 'hex').toString('base64url'), e: 'AQAB' }, format: 'jwk' });
148
+ const key = { key: pub, padding: constants.RSA_PKCS1_PADDING };
149
+ const chunks = [];
150
+ for (let o = 0; o < payload.length; o += 117)
151
+ chunks.push(publicEncrypt(key, payload.subarray(o, o + 117)));
152
+ return Buffer.concat(chunks).toString('latin1');
153
+ }
154
+ // ---------------------------------------------------------------- RWP 信封
155
+ /** message_id:uuid4().hex[:14] + '-' + hex(now)[-11:]。 */
156
+ export const messageId = () => rand.uuid4().replaceAll('-', '').slice(0, 14) + '-' + rand.now().toString(16).slice(-11);
157
+ const envelope = (t, m, b) => JSON.stringify({ v: 1, t, ...(m != null ? { m } : {}), ...(b !== undefined ? { b } : {}) });
158
+ const b64 = (v) => Buffer.from(v).toString('base64');
159
+ export function handshakeFrame(id) {
160
+ const m = messageId();
161
+ const payload = {
162
+ appId: 'xhs-pc',
163
+ authInfo: { authType: 'generic', sid: id.sid, uid: id.uid, domain: 'red' },
164
+ deviceInfo: { deviceId: id.deviceId, fingerprint: id.fingerprint, platform: 'browser', os: 'web', osVersion: '10.15', deviceName: 'Chrome', appVersion: '131.0.0.0', userAgent: UA },
165
+ serviceTag: '',
166
+ bizInfos: [
167
+ { bizName: 'dqa_chatsearch', serializeType: 'protobuf' },
168
+ { bizName: 'xhs_dots_pc', serializeType: 'protobuf' },
169
+ { bizName: 'push', serializeType: 'json' },
170
+ ],
171
+ roomInfo: [],
172
+ roomInfos: [],
173
+ tagInfo: [],
174
+ extInfo: {},
175
+ state: 1,
176
+ };
177
+ return [envelope(2, m, { d: { a: 1, s: 0, b: payload } }), m];
178
+ }
179
+ export const registerFrame = (biz, type) => envelope(2, messageId(), { d: { a: 1, s: 1, b: { bizInfo: { bizName: biz, serializeType: type }, register: true } } });
180
+ export const joinRoomFrame = (roomId) => envelope(2, messageId(), { d: { a: 1, s: 8, b: { info: { bizName: 'room', roomId, roomType: 'LIVE' } } } });
181
+ export const heartbeatFrame = () => envelope(0);
182
+ export const stateSyncFrame = () => envelope(2, messageId(), { d: { a: 1, s: 6, b: {} } });
183
+ /** im_frame:客户端发私信用 action=2、服务 rrmp.b.i。 */
184
+ export const imFrame = (payload) => envelope(3, messageId(), { d: { a: 2, c: 'sendMessage', biz: 'im', b: b64(payload), e: {}, s: 'rrmp.b.i' } });
185
+ /** 直播间观看心跳(上游 heartbeat)。 */
186
+ export function viewerHeartFrame(roomId, profile) {
187
+ const custom = { type: 'viewer_heart', priority: 0, profile, source: 'web_live', desc: '' };
188
+ const data = { roomId, roomType: 'LIVE', command: 1, customData: JSON.stringify(custom) };
189
+ return envelope(3, messageId(), { d: { a: 0, c: 'liveHeartBeat', biz: 'room', b: b64(JSON.stringify(data)), e: {}, s: 'rrmp.o.l' } });
190
+ }
191
+ /**
192
+ * encode_room_text_payload + room_text_frame:直播间文本弹幕(t=3,biz=room,c=sendMessage,a=0,s=rrmp.o.l)。
193
+ * 上游要求调用方给出 command / priority / role;这里用与观看心跳(viewer_heart)相同的 1 / 0 / 0。
194
+ * 返回帧和它的 m(服务端的回执带同一个 m)。
195
+ */
196
+ export function roomTextFrame(t) {
197
+ const custom = { type: 'text', priority: t.priority ?? 0, profile: { nickname: t.nickname, avatar: t.avatar, user_id: t.userId, role: t.role ?? 0 }, desc: t.content };
198
+ const payload = compactJson({ roomId: t.roomId, roomType: t.roomType ?? 'LIVE', command: t.command ?? 1, customData: compactJson(custom) });
199
+ const m = messageId();
200
+ return [envelope(3, m, { d: { a: 0, c: 'sendMessage', biz: 'room', b: b64(payload), e: {}, s: 'rrmp.o.l' } }), m];
201
+ }
202
+ /** decode_room_push:t=4 帧里 biz=room 的事件(base64 JSON,customData 再解一层)。 */
203
+ export function decodeRoomPush(frame) {
204
+ const d = frame?.t === 4 ? frame?.b?.d : null;
205
+ if (!d || d.biz !== 'room' || !Array.isArray(d.b))
206
+ return [];
207
+ const out = [];
208
+ for (const item of d.b) {
209
+ if (typeof item?.d !== 'string')
210
+ continue;
211
+ try {
212
+ const payload = jsonLoads(Buffer.from(item.d, 'base64').toString('utf8'));
213
+ if (typeof payload?.customData === 'string') {
214
+ try {
215
+ payload.customData = jsonLoads(payload.customData);
216
+ }
217
+ catch { }
218
+ }
219
+ out.push(payload);
220
+ }
221
+ catch { }
222
+ }
223
+ return out;
224
+ }
225
+ /** IM 帧:t=3 的 b.d.b 是单个 base64,t=4 的 b.d.b 是 [{d}] 批量;发送回执在 b.a.b。 */
226
+ export function decodeImFrame(frame) {
227
+ const out = [];
228
+ const d = frame?.b?.d;
229
+ const tryDecode = (s) => {
230
+ if (typeof s !== 'string' || !s)
231
+ return;
232
+ try {
233
+ out.push(decodeChat(Buffer.from(s, 'base64')));
234
+ }
235
+ catch { }
236
+ };
237
+ if (d?.biz === 'im') {
238
+ if (typeof d.b === 'string')
239
+ tryDecode(d.b);
240
+ else if (Array.isArray(d.b))
241
+ for (const item of d.b)
242
+ tryDecode(item?.d);
243
+ }
244
+ if (!out.length)
245
+ tryDecode(frame?.b?.a?.b);
246
+ return out;
247
+ }
248
+ /** 长连的等待时限(毫秒):握手回执、发送回执(上游 _wait_transport_ack 的 10 秒)。测试里调短。 */
249
+ export const PUSH_TIMEOUTS = { handshake: 10_000, ack: 10_000 };
250
+ const TIMEOUT = Symbol('timeout');
251
+ /** ms 毫秒后兑现为 TIMEOUT 的定时器(真实时间,不受 rand 的假时钟影响);用完 cancel。 */
252
+ function timer(ms) {
253
+ let t;
254
+ const expired = new Promise((resolve) => (t = setTimeout(() => resolve(TIMEOUT), ms)));
255
+ return { expired, cancel: () => clearTimeout(t) };
256
+ }
257
+ /**
258
+ * 在 ms 毫秒内等 pick 认领一帧(返回非 undefined 的值)。超时或连接断开时返回 undefined。
259
+ * 超时后那次挂起的读取还在,会吞掉下一帧,所以这条连接不能再读,调用方随即 close。
260
+ */
261
+ export async function awaitFrame(conn, ms, pick) {
262
+ const frames = conn.frames[Symbol.asyncIterator]();
263
+ const wait = timer(ms);
264
+ try {
265
+ for (;;) {
266
+ const r = await Promise.race([frames.next(), wait.expired]);
267
+ if (r === TIMEOUT || r.done)
268
+ return undefined;
269
+ const v = pick(r.value);
270
+ if (v !== undefined)
271
+ return v;
272
+ }
273
+ }
274
+ finally {
275
+ wait.cancel();
276
+ }
277
+ }
278
+ /**
279
+ * 上游 connect_push_from_storage:没有 RWP token 时先 celestial/lt 换一个,再握手、注册 im(可选进直播间)。
280
+ * 握手回执最多等 PUSH_TIMEOUTS.handshake;握手没成功时关掉连接再抛错。
281
+ */
282
+ export async function connectPush(p, signal, roomId) {
283
+ const tokenValid = () => p.state.rwpToken && p.state.rwpToken.uid === p.userId && (p.state.rwpToken.expiredAt == null || p.state.rwpToken.expiredAt > rand.now());
284
+ if (!tokenValid()) {
285
+ p.state.rwpToken = null;
286
+ p.check(await celestialLt(p));
287
+ }
288
+ const sid = p.state.rwpToken?.aLt ?? '';
289
+ if (!sid || !p.userId)
290
+ throw new SpecificAIError('AUTH_REQUIRED', '没有拿到私信长连的登录 token(celestial/lt)', { hint: 'specific-ai xhs auth login' });
291
+ const id = { uid: p.userId, sid, deviceId: p.state.tabDeviceId, fingerprint: p.state.rwpFingerprint };
292
+ const cookie = Object.entries(p.wire(PUSH_URL.replace('wss://', 'https://'))).map(([k, v]) => `${k}=${v}`).join('; ');
293
+ const socket = await openSocket(PUSH_URL, { headers: { 'User-Agent': UA, Origin: WEB, ...(cookie ? { Cookie: cookie } : {}) }, signal });
294
+ const it = socket.messages[Symbol.asyncIterator]();
295
+ const pending = [];
296
+ const parse = (raw) => {
297
+ try {
298
+ return jsonLoads(String(raw));
299
+ }
300
+ catch {
301
+ return { raw: String(raw) };
302
+ }
303
+ };
304
+ try {
305
+ const [hs, hsId] = handshakeFrame(id);
306
+ await socket.send(hs);
307
+ // 浏览器等握手的 c=0 回执后才注册,否则后续帧都会被拒(3100001 Account has not privilege)
308
+ const wait = timer(PUSH_TIMEOUTS.handshake);
309
+ try {
310
+ for (;;) {
311
+ const r = await Promise.race([it.next(), wait.expired]);
312
+ if (r === TIMEOUT)
313
+ throw new SpecificAIError('NETWORK', `私信长连握手超时(${PUSH_TIMEOUTS.handshake / 1000} 秒没有回执)`, { detail: { kind: 'timeout' } });
314
+ if (r.done)
315
+ throw new SpecificAIError('NETWORK', '私信长连在握手时断开');
316
+ const f = parse(r.value);
317
+ if (f?.m !== hsId) {
318
+ pending.push(f);
319
+ continue;
320
+ }
321
+ const c = f?.b?.a?.c;
322
+ if (c !== 0)
323
+ throw new SpecificAIError('AUTH_EXPIRED', `私信长连握手被拒绝:c=${c} ${f?.b?.a?.m ?? ''}`, { hint: 'specific-ai xhs auth login' });
324
+ break;
325
+ }
326
+ }
327
+ finally {
328
+ wait.cancel();
329
+ }
330
+ await socket.send(registerFrame('im', 'protobuf'));
331
+ if (roomId) {
332
+ await socket.send(registerFrame('room', 'json'));
333
+ await socket.send(joinRoomFrame(roomId));
334
+ }
335
+ }
336
+ catch (err) {
337
+ socket.close();
338
+ throw err;
339
+ }
340
+ const beat = setInterval(() => void socket.send(heartbeatFrame()).catch(() => { }), 30_000).unref();
341
+ return {
342
+ socket,
343
+ send: (frame) => socket.send(frame),
344
+ close: () => {
345
+ clearInterval(beat);
346
+ socket.close();
347
+ },
348
+ frames: {
349
+ async *[Symbol.asyncIterator]() {
350
+ while (pending.length)
351
+ yield pending.shift();
352
+ for (;;) {
353
+ const r = await it.next();
354
+ if (r.done)
355
+ return;
356
+ yield parse(r.value);
357
+ }
358
+ },
359
+ },
360
+ };
361
+ }