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,133 @@
1
+ import { SpecificAIError } from '../../../core/errors.js';
2
+ import { md5Hex } from '../../../core/hash.js';
3
+ import { quote } from '../../../core/py.js';
4
+ import * as rand from '../../../core/rand.js';
5
+ import * as api from './api.js';
6
+ import { ORIGIN } from './profile.js';
7
+ import { imConversationRequest, imOuterRequest, imSendBody, imSendFrame, imUserInitRequest } from './wire.js';
8
+ import { frontierSign } from './jsrun.js';
9
+ /**
10
+ * 私信(上游 api/tiktok_web.py 的 IM 部分)。
11
+ *
12
+ * 上游要求浏览器会话里的 im_headers(32 个键的有序 map)、im_sequence_id、im_config_id 等运行时字段,缺了就拒绝。
13
+ * specific-ai 优先用会话 JSON 里的 browser_metrics;没有时 im_headers 按与 query 相同的浏览器画像补齐,
14
+ * sequence_id 从 10000 起递增,config_id 缺省时不发(与上游 `None` 时不写字段一致)。
15
+ */
16
+ export const IM_HEADER_KEYS = [
17
+ 'aid', 'app_name', 'channel', 'device_platform', 'device_id', 'region', 'priority_region', 'os', 'referer', 'root_referer', 'cookie_enabled',
18
+ 'screen_width', 'screen_height', 'browser_language', 'browser_platform', 'browser_name', 'browser_version', 'browser_online', 'verifyFp',
19
+ 'app_language', 'webcast_language', 'tz_name', 'is_page_visible', 'focus_state', 'is_fullscreen', 'history_len', 'user_is_login',
20
+ 'data_collection_enabled', 'from_appID', 'locale', 'user_agent', 'Web-Sdk-Ms-Token',
21
+ ];
22
+ /** Request.headers 的有序 map(上游 _im_header_map)。 */
23
+ export function imHeaders(t) {
24
+ const source = t.metrics.im_headers;
25
+ if (source && typeof source === 'object') {
26
+ const missing = IM_HEADER_KEYS.filter((k) => !(k in source));
27
+ if (missing.length)
28
+ throw new SpecificAIError('ERROR', `会话里的 im_headers 缺少字段:${missing.join(', ')}`);
29
+ return IM_HEADER_KEYS.map((k) => [k, String(source[k])]);
30
+ }
31
+ const values = {
32
+ aid: '1988',
33
+ app_name: 'tiktok_web',
34
+ channel: 'tiktok_web',
35
+ device_platform: 'web_pc',
36
+ device_id: t.deviceId,
37
+ region: t.region,
38
+ priority_region: t.priorityRegion,
39
+ os: 'windows',
40
+ referer: api.MESSAGES_PAGE,
41
+ root_referer: api.MESSAGES_PAGE,
42
+ cookie_enabled: 'true',
43
+ screen_width: '2560',
44
+ screen_height: '1440',
45
+ browser_language: 'zh-CN',
46
+ browser_platform: 'Win32',
47
+ browser_name: 'Mozilla',
48
+ browser_version: t.ua.split('Mozilla/').slice(1).join('Mozilla/'),
49
+ browser_online: 'true',
50
+ verifyFp: t.verifyFp,
51
+ app_language: 'zh-Hans',
52
+ webcast_language: 'zh-Hans',
53
+ tz_name: 'Asia/Shanghai',
54
+ is_page_visible: 'true',
55
+ focus_state: 'true',
56
+ is_fullscreen: 'false',
57
+ history_len: '4',
58
+ user_is_login: 'true',
59
+ data_collection_enabled: 'true',
60
+ from_appID: '1988',
61
+ locale: 'zh-Hans',
62
+ user_agent: t.ua,
63
+ 'Web-Sdk-Ms-Token': t.msToken,
64
+ };
65
+ return IM_HEADER_KEYS.map((k) => [k, values[k]]);
66
+ }
67
+ /** 下一个 sequence_id:会话里给了就从它开始,之后每次 +1(存在凭证里)。 */
68
+ function nextSequence(t, key) {
69
+ const extra = t.ctx.credential.extra;
70
+ const stored = Number(extra[key] ?? t.metrics[key] ?? 10000);
71
+ extra[key] = stored + 1;
72
+ return stored;
73
+ }
74
+ /** 私信 HTTP 拉取请求的公共外层字段。 */
75
+ export function envelope(t) {
76
+ const config = t.metrics.im_config_id;
77
+ return { sequenceId: nextSequence(t, 'im_sequence_id'), deviceId: t.deviceId, headers: imHeaders(t), configId: config == null ? null : Number(config) };
78
+ }
79
+ /** cmd 203:私信页初次拉取(会话与最近消息)。翻页时 cursor 用上一页回包的 next_cursor。 */
80
+ export async function pullInit(t, cursor = 0) {
81
+ return api.postImProtobuf(t, '/v2/message/get_by_user_init', imUserInitRequest(cursor, envelope(t)));
82
+ }
83
+ /** cmd 301:一个会话的消息。 */
84
+ export async function pullConversation(t, c) {
85
+ const raw = imConversationRequest({ conversationId: c.conversationId, shortId: c.shortId, type: c.type, anchorIndex: c.anchorIndex ?? '0', direction: c.direction ?? 1, limit: c.limit ?? 50 }, envelope(t));
86
+ return api.postImProtobuf(t, '/v1/message/get_by_conversation', raw);
87
+ }
88
+ export function convCache(t) {
89
+ return (t.ctx.credential.extra.im_conversations ??= {});
90
+ }
91
+ // ---------------------------------------------------------------- WebSocket
92
+ /** WS 的 access_key:md5("9" + app_key + wid + 盐),wid 来自 privacy config(上游 build_im_access_key)。 */
93
+ export async function imWsUrl(t) {
94
+ let wid = String(t.metrics.im_wid ?? '');
95
+ if (!wid) {
96
+ const body = await api.cookiePrivacyConfig(t);
97
+ wid = String(body?.body?.consent?.wid ?? '');
98
+ if (!wid)
99
+ throw new SpecificAIError('UPSTREAM', 'TikTok privacy config 缺少 body.consent.wid');
100
+ }
101
+ const accessKey = md5Hex(`9e1bd35ec9db7b8d846de66ed140b1ad9${wid}f8a69f1719916z`);
102
+ const ttwid = t.ttwid;
103
+ if (!ttwid)
104
+ throw new SpecificAIError('AUTH_REQUIRED', '私信 WebSocket 缺少 ttwid cookie', { hint: 'specific-ai tiktok auth login' });
105
+ return (`wss://im-ws-${t.region.toLowerCase()}.tiktok.com/ws/v2?device_platform=web&version_code=fws_1.0.0&access_key=${accessKey}` +
106
+ `&fpid=9&aid=1459&ttwid=${quote(ttwid, '|~-._')}&xsack=1&xaack=1&xsqos=0`);
107
+ }
108
+ /** cmd 100 的完整帧:Request 里带现签的 ticket-guard,帧头带 frontierSign(md5(Request))(上游 send_im_message)。 */
109
+ export async function buildSendFrame(t, c) {
110
+ const normal = imHeaders(t);
111
+ const guard = t.ticketGuard('/ws/v2');
112
+ const requestHeaders = [...normal];
113
+ for (const [k, v] of Object.entries(guard)) {
114
+ const i = requestHeaders.findIndex(([key]) => key === k);
115
+ if (i >= 0)
116
+ requestHeaders[i] = [k, v];
117
+ else
118
+ requestHeaders.push([k, v]);
119
+ }
120
+ const clientMessageId = rand.uuid4();
121
+ const body = imSendBody({ conversationId: c.conversationId, shortId: c.shortId, type: c.type, text: c.text, clientMessageId });
122
+ const sequenceId = nextSequence(t, 'im_ws_sequence_id');
123
+ const request = imOuterRequest(100, body, { sequenceId, deviceId: t.deviceId, devicePlatform: 'web', headers: requestHeaders, authType: 1 });
124
+ const marker = await frontierSign({
125
+ cookie: t.device.document_cookie || t.cookieStr,
126
+ userAgent: t.ua,
127
+ referer: String(t.metrics.im_page || `${ORIGIN}/messages`),
128
+ metrics: t.metrics,
129
+ stub: md5Hex(request),
130
+ });
131
+ const frame = imSendFrame({ request, sequenceId, logId: rand.now(), xBogus: marker, frameHeaders: normal });
132
+ return { frame, request, body, clientMessageId, sequenceId };
133
+ }
@@ -0,0 +1,126 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { join } from 'node:path';
3
+ import { SpecificAIError } from '../../../core/errors.js';
4
+ import { PACKAGE_ROOT, staticFile } from '../../../core/paths.js';
5
+ import * as rand from '../../../core/rand.js';
6
+ /**
7
+ * 上游的两个签名运行器(AGENTS 7.4 的子进程退路):
8
+ * - static/tiktok/signing/env/sign.js:WebMssdk 的 frontierSign(直播 / 私信 WebSocket 的 16 位 X-Bogus);
9
+ * - static/tiktok/shop_bsid/sign.js:TikTok Shop 的 OEC Lucifer BSID。
10
+ *
11
+ * 两者都改写进程级全局(global.window、Function.prototype.toString、隐藏 process 等),放不进 node:vm,
12
+ * 所以和上游一样每次起一个 node 子进程,按包内路径运行原样复制的 JS。
13
+ *
14
+ * 确定性模式(对拍)下预加载 scripts/golden/tiktok/node_determinism.cjs,与上游对拍时用的是同一个文件。
15
+ */
16
+ async function runNode(script, input, env = {}) {
17
+ const deterministic = rand.isDeterministic();
18
+ const extra = deterministic
19
+ ? {
20
+ NODE_OPTIONS: `--require "${join(PACKAGE_ROOT, 'scripts', 'golden', 'tiktok', 'node_determinism.cjs').replaceAll('\\', '/')}"`,
21
+ SPECIFIC_AI_GOLDEN_SEED: String(rand.DEFAULT_SEED),
22
+ SPECIFIC_AI_GOLDEN_NOW: String(rand.now()),
23
+ }
24
+ : { NODE_OPTIONS: '' };
25
+ return new Promise((resolve, reject) => {
26
+ const child = spawn(process.execPath, [script], {
27
+ env: { ...process.env, ...extra, ...env },
28
+ stdio: ['pipe', 'pipe', 'pipe'],
29
+ windowsHide: true,
30
+ });
31
+ const out = [];
32
+ const err = [];
33
+ const timer = setTimeout(() => child.kill(), 30_000);
34
+ child.stdout.on('data', (d) => out.push(d));
35
+ child.stderr.on('data', (d) => err.push(d));
36
+ child.on('error', (e) => {
37
+ clearTimeout(timer);
38
+ reject(new SpecificAIError('ERROR', `签名进程启动失败:${e.message}`));
39
+ });
40
+ child.on('close', (code) => {
41
+ clearTimeout(timer);
42
+ const stdout = Buffer.concat(out).toString('utf8');
43
+ if (code !== 0 && !stdout.trim()) {
44
+ // 运行器的 stderr 可能带着 cookie,只取最后一行错误类型
45
+ const reason = Buffer.concat(err).toString('utf8').trim().split(/\r?\n/).reverse().find((l) => /(Error|error):|is required|failed|length/.test(l)) ?? '';
46
+ reject(new SpecificAIError('ERROR', `签名进程失败${reason ? `:${reason.slice(0, 200)}` : ''}`));
47
+ }
48
+ else
49
+ resolve(stdout);
50
+ });
51
+ child.stdin.end(input);
52
+ });
53
+ }
54
+ export async function frontierSign(o) {
55
+ const request = { mode: 'frontier_sign', url: o.referer, cookie: o.cookie, user_agent: o.userAgent, referer: o.referer, metrics: o.metrics ?? {}, headers: {} };
56
+ if (o.stub != null) {
57
+ if (!/^[0-9a-f]{32}$/.test(o.stub))
58
+ throw new SpecificAIError('ERROR', 'IM X-MS-STUB 必须为 32 位小写 MD5');
59
+ request.stub = o.stub;
60
+ }
61
+ const stdout = await runNode(staticFile('tiktok', 'signing/env/sign.js'), JSON.stringify(request) + '\n');
62
+ const replies = stdout.split(/\r?\n/).filter((l) => l.startsWith('{"ok":'));
63
+ let result = {};
64
+ try {
65
+ result = replies.length ? JSON.parse(replies[replies.length - 1]) : {};
66
+ }
67
+ catch { }
68
+ const marker = result?.result?.values?.['X-Bogus'];
69
+ if (!result.ok || typeof marker !== 'string' || marker.length !== 16) {
70
+ throw new SpecificAIError('ERROR', `本地 frontierSign 未产生长度 16 的 X-Bogus${result.error ? `:${String(result.error).slice(0, 200)}` : ''}`);
71
+ }
72
+ return marker;
73
+ }
74
+ // ---------------------------------------------------------------- Shop BSID(signing/shop_bsid.py ShopBSIDSigner)
75
+ export const SHOP_BSID_LENGTH = 382;
76
+ const BROWSER_MS_TOKEN_LENGTHS = [144, 152];
77
+ export async function shopSign(o) {
78
+ const oec = /(?:^|;\s*)oec_lucifer=([^;]+)/.exec(o.cookie)?.[1];
79
+ if (!oec || !/^[0-9a-fA-F]{160}$/.test(oec)) {
80
+ throw new SpecificAIError('AUTH_REQUIRED', '当前 Cookie 缺少浏览器生成的 160 位 oec_lucifer(在 shop.tiktok.com 打开任一商品页后重新导出 Cookie)', {
81
+ hint: 'specific-ai tiktok auth login --cookie @tiktok-session.json',
82
+ });
83
+ }
84
+ const msToken = [...o.cookie.matchAll(/(?:^|;\s*)msToken=([^;]+)/g)].map((m) => decodeURIComponent(m[1])).find((v) => BROWSER_MS_TOKEN_LENGTHS.includes(v.length)) ?? '';
85
+ if (!/^[0-9A-Za-z_-]+={0,2}$/.test(msToken)) {
86
+ throw new SpecificAIError('AUTH_REQUIRED', '当前 Cookie 缺少 Shop 请求使用的 144/152 位 msToken(在 shop.tiktok.com 打开商品页后重新导出 Cookie)', {
87
+ hint: 'specific-ai tiktok auth login --cookie @tiktok-session.json',
88
+ });
89
+ }
90
+ if (!o.userAgent)
91
+ throw new SpecificAIError('ERROR', 'Shop X-Bogus 纯算需要完整 user-agent');
92
+ const sep = o.url.includes('?') ? '&' : '?';
93
+ // Chrome 的 pre-sign URL 里 base64 的 == 原样保留
94
+ const signingUrl = `${o.url}${sep}msToken=${encodeURIComponent(msToken).replace(/%3D/g, '=')}`;
95
+ const payload = {
96
+ url: signingUrl,
97
+ method: o.method.toUpperCase(),
98
+ headers: o.headers,
99
+ body: o.body,
100
+ cookie: o.cookie,
101
+ expected_length: SHOP_BSID_LENGTH,
102
+ expected_ms_token_length: msToken.length,
103
+ x_bogus: o.xBogus(msToken),
104
+ };
105
+ const stdout = await runNode(staticFile('tiktok', 'shop_bsid/sign.js'), JSON.stringify(payload), { TIKTOK_BSID_QUIET: '1' });
106
+ let result;
107
+ try {
108
+ result = JSON.parse(stdout);
109
+ }
110
+ catch {
111
+ throw new SpecificAIError('ERROR', 'Shop BSID 运行器返回了非 JSON 数据');
112
+ }
113
+ const bsid = String(result.bsid ?? '');
114
+ if (bsid.length !== SHOP_BSID_LENGTH || !/^[0-9a-f]+$/.test(bsid) || String(result.length) !== String(SHOP_BSID_LENGTH)) {
115
+ throw new SpecificAIError('ERROR', 'Shop BSID 输出格式或长度与 Chrome 不一致');
116
+ }
117
+ return {
118
+ signed_url: String(result.signed_url ?? ''),
119
+ bsid,
120
+ length: SHOP_BSID_LENGTH,
121
+ pre_sign_lengths: result.pre_sign_lengths ?? {},
122
+ pre_sign_url_length: Number(result.pre_sign_url_length ?? 0),
123
+ pre_sign_order: result.pre_sign_order ?? [],
124
+ pre_sign_url: String(result.pre_sign_url ?? ''),
125
+ };
126
+ }
@@ -0,0 +1,286 @@
1
+ import * as n from '../../../core/normalize.js';
2
+ import { jsonLoads } from '../../../core/py.js';
3
+ import { ORIGIN, SHOP } from './profile.js';
4
+ /** TikTok 原始对象 → 归一化类型(AGENTS 6.2)。 */
5
+ export const userUrl = (uniqueId) => (uniqueId ? `${ORIGIN}/@${uniqueId}` : null);
6
+ export const itemUrl = (uniqueId, id, photo = false) => `${ORIGIN}/@${uniqueId || '_'}/${photo ? 'photo' : 'video'}/${id}`;
7
+ export const liveUrl = (uniqueId) => (uniqueId ? `${ORIGIN}/@${uniqueId}/live` : null);
8
+ export const productUrl = (id) => `${SHOP}/view/product/${id}`;
9
+ export function ref(id, name, uniqueId) {
10
+ return n.userRef({ id, name: name || uniqueId, url: userUrl(uniqueId) });
11
+ }
12
+ const first = (v) => (Array.isArray(v) ? n.url(v[0]) : n.url(v));
13
+ /** 作品(itemStruct:视频页 hydration、搜索、推荐、主页列表都用这个结构)。 */
14
+ export function item(v) {
15
+ const a = v.author ?? {};
16
+ const photo = Boolean(v.imagePost);
17
+ const s = { ...(v.stats ?? {}), ...(v.statsV2 ?? {}) };
18
+ return n.item({
19
+ id: n.id(v.id),
20
+ kind: photo ? 'image' : 'video',
21
+ url: itemUrl(a.uniqueId, v.id, photo),
22
+ title: n.str(v.imagePost?.title),
23
+ text: n.str(v.desc),
24
+ author: ref(a.id, a.nickname, a.uniqueId),
25
+ created_at: n.time(v.createTime),
26
+ cover: n.url(v.video?.cover || v.video?.originCover) ?? first(v.imagePost?.cover?.imageURL?.urlList),
27
+ media: itemMedia(v),
28
+ stats: {
29
+ views: n.count(s.playCount),
30
+ likes: n.count(s.diggCount),
31
+ comments: n.count(s.commentCount),
32
+ collects: n.count(s.collectCount),
33
+ shares: n.count(s.shareCount),
34
+ },
35
+ status: v.privateItem || v.secret ? 'private' : null,
36
+ }, v);
37
+ }
38
+ /**
39
+ * 可播放 / 下载的媒体:视频取 playAddr(同源 h264),图文取每张图。
40
+ * 背景音乐:图文总是附上(图文的声音只在音乐里);视频只在 `withMusic` 时附上(`item media` 要,Item.media 与下载不要)。
41
+ */
42
+ export function itemMedia(v, withMusic = false) {
43
+ const out = [];
44
+ const images = v.imagePost?.images ?? [];
45
+ if (images.length) {
46
+ images.forEach((img, i) => {
47
+ const url = first(img.imageURL?.urlList);
48
+ if (url)
49
+ out.push(n.media({ id: String(i + 1), type: 'image', url, width: n.count(img.imageWidth), height: n.count(img.imageHeight) }, img));
50
+ });
51
+ }
52
+ else if (v.video) {
53
+ const url = n.url(v.video.playAddr) ?? n.url(v.video.downloadAddr) ?? first(v.video.bitrateInfo?.[0]?.PlayAddr?.UrlList);
54
+ if (url) {
55
+ out.push(n.media({ id: n.id(v.video.id || v.id), type: 'video', url, width: n.count(v.video.width), height: n.count(v.video.height), duration: n.seconds(v.video.duration) }, v.video));
56
+ }
57
+ }
58
+ if ((images.length || withMusic) && v.music?.playUrl)
59
+ out.push(n.media({ id: n.id(v.music.id), type: 'audio', url: n.url(v.music.playUrl), duration: n.seconds(v.music.duration) }, v.music));
60
+ return out;
61
+ }
62
+ /** 用户(user-detail 的 userInfo,或列表里的 {user, stats})。 */
63
+ export function user(info) {
64
+ const u = info.user ?? info;
65
+ const s = { ...(info.stats ?? {}), ...(info.statsV2 ?? {}) };
66
+ return n.user({
67
+ id: n.id(u.id ?? u.uid),
68
+ name: n.str(u.nickname ?? u.nickName),
69
+ handle: n.str(u.uniqueId ?? u.unique_id),
70
+ avatar: n.url(u.avatarLarger ?? u.avatarMedium ?? u.avatarThumb),
71
+ url: userUrl(u.uniqueId ?? u.unique_id),
72
+ bio: n.str(u.signature),
73
+ stats: {
74
+ followers: n.count(s.followerCount),
75
+ following: n.count(s.followingCount),
76
+ items: n.count(s.videoCount),
77
+ likes: n.count(s.heartCount ?? s.heart),
78
+ },
79
+ }, info);
80
+ }
81
+ export function comment(v, itemId) {
82
+ const u = v.user ?? {};
83
+ const parent = v.reply_id && v.reply_id !== '0' ? n.id(v.reply_id) : null;
84
+ return n.comment({
85
+ id: n.id(v.cid),
86
+ item_id: n.id(v.aweme_id ?? itemId),
87
+ parent_id: parent,
88
+ author: ref(u.uid, u.nickname, u.unique_id),
89
+ text: String(v.text ?? ''),
90
+ created_at: n.time(v.create_time),
91
+ stats: { likes: n.count(v.digg_count), replies: n.count(v.reply_comment_total) },
92
+ }, v);
93
+ }
94
+ /** 商品评价(Shop PDP 的 product_reviews 项)。 */
95
+ export function review(v, productId) {
96
+ return n.comment({
97
+ id: n.id(v.review_id ?? v.id),
98
+ item_id: productId,
99
+ author: n.userRef({ id: v.review_user?.user_id ?? v.user_id ?? v.review_user?.name, name: v.review_user?.name ?? v.user_name }),
100
+ text: String(v.review_text ?? v.display_text ?? v.text ?? ''),
101
+ created_at: n.time(v.review_timestamp ?? v.review_time ?? v.create_time),
102
+ stats: { likes: n.count(v.like_count ?? v.helpful_count), replies: null },
103
+ }, v);
104
+ }
105
+ /** 商品(Shop PDP SSR 的 product_info.component_data)。 */
106
+ export function product(d) {
107
+ const p = d.product_info?.product_model ?? {};
108
+ const promo = d.product_info?.promotion_model ?? d.promotion_model ?? {};
109
+ const seller = d.product_info?.seller_model ?? d.seller_model ?? d.shop_info ?? {};
110
+ const price = promo.promotion_product_price ?? promo.min_price ?? p.min_price ?? {};
111
+ const images = p.images ?? [];
112
+ const amount = price.sale_price_decimal ?? price.real_price ?? price.min_sku_price ?? price.price_val;
113
+ return n.item({
114
+ id: n.id(p.product_id),
115
+ kind: 'goods',
116
+ url: productUrl(p.product_id),
117
+ title: n.str(p.name ?? p.title),
118
+ text: n.str(p.description_text ?? null),
119
+ author: n.userRef({ id: seller.seller_id ?? seller.shop_id, name: seller.name ?? seller.shop_name }),
120
+ cover: first(images[0]?.url_list),
121
+ media: images.flatMap((img, i) => {
122
+ const url = first(img.url_list);
123
+ return url ? [n.media({ id: String(i + 1), type: 'image', url, width: n.count(img.width), height: n.count(img.height) }, img)] : [];
124
+ }),
125
+ stats: { comments: n.count(d.review_info?.total_reviews ?? p.review_count), views: null, likes: null, collects: null, shares: null },
126
+ price: n.price(amount, price.currency ?? price.currency_name ?? 'USD'),
127
+ status: p.status === 1 || p.status == null ? 'on_sale' : 'off_shelf',
128
+ }, d);
129
+ }
130
+ export function folder(v) {
131
+ return n.folder({ id: n.id(v.collectionId), name: String(v.name ?? ''), count: n.count(v.total), url: v.userName ? `${ORIGIN}/@${v.userName}/collection/${encodeURIComponent(v.name ?? '')}-${v.collectionId}` : null }, v);
132
+ }
133
+ export function series(v) {
134
+ const id = n.id(v.mixId ?? v.id);
135
+ const creator = v.creator?.uniqueId;
136
+ return n.folder({ id, name: String(v.mixName ?? v.name ?? ''), count: n.count(v.videoCount), url: creator ? `${ORIGIN}/@${creator}/playlist/${encodeURIComponent(v.mixName ?? v.name ?? '')}-${id}` : null }, v);
137
+ }
138
+ // ---------------------------------------------------------------- 直播
139
+ /** /api-live/user/room 的 data:user + liveRoom。status 2 为直播中。 */
140
+ export function liveRoom(d) {
141
+ const u = d.user ?? {};
142
+ const r = d.liveRoom ?? {};
143
+ return n.live({
144
+ id: n.id(u.roomId || r.roomId),
145
+ url: liveUrl(u.uniqueId),
146
+ title: n.str(r.title),
147
+ status: Number(r.status ?? u.status) === 2 ? 'live' : 'offline',
148
+ host: ref(u.id, u.nickname, u.uniqueId),
149
+ cover: n.url(r.coverUrl),
150
+ stats: { viewers: n.count(r.liveRoomStats?.userCount) },
151
+ }, d);
152
+ }
153
+ /** webcast 接口的房间对象(feed、搜索):snake_case。 */
154
+ export function webcastRoom(r) {
155
+ const o = r.owner ?? {};
156
+ return n.live({
157
+ id: n.id(r.id_str ?? r.id),
158
+ url: liveUrl(o.display_id),
159
+ title: n.str(r.title),
160
+ status: Number(r.status) === 2 ? 'live' : 'offline',
161
+ host: ref(o.id_str ?? o.id, o.nickname, o.display_id),
162
+ cover: first(r.cover?.url_list),
163
+ stats: { viewers: n.count(r.user_count ?? r.stats?.total_user) },
164
+ }, r);
165
+ }
166
+ /** 画质的顺序:原画在前,纯音频(ao)在最后。 */
167
+ const QUALITIES = ['origin', 'uhd', 'hd', 'sd', 'ld', 'md', 'ao'];
168
+ const PROTOCOLS = ['flv', 'hls', 'cmaf', 'dash', 'lls'];
169
+ function parseJson(v) {
170
+ if (typeof v !== 'string')
171
+ return v ?? null;
172
+ try {
173
+ return jsonLoads(v);
174
+ }
175
+ catch {
176
+ return null;
177
+ }
178
+ }
179
+ /**
180
+ * 拉流地址(上游没有解析)。两种来源:
181
+ * - `pull_data.stream_data`:JSON 串,`data.<画质>.main.{flv,hls,...}`,`sdk_params`(JSON 串)里有 resolution。
182
+ * /api-live/user/room 在 `liveRoom.streamData`,webcast 房间对象在 `stream_url.live_core_sdk_data`;
183
+ * - webcast 房间对象的 `stream_url.flv_pull_url`(画质 → 地址)、`hls_pull_url_map`、`hls_pull_url`、`rtmp_pull_url`。
184
+ */
185
+ export function liveStreams(source) {
186
+ const out = [];
187
+ const seen = new Set();
188
+ const push = (id, url, raw, size) => {
189
+ const u = n.url(url);
190
+ if (!u || seen.has(u))
191
+ return;
192
+ seen.add(u);
193
+ const [w, h] = /^(\d+)x(\d+)$/.exec(String(size ?? ''))?.slice(1) ?? [];
194
+ out.push(n.media({ id, type: id.startsWith('ao-') ? 'audio' : 'video', url: u, width: n.count(w), height: n.count(h) }, raw));
195
+ };
196
+ // 可能是 {pull_data}、{live_core_sdk_data},也可能已经是 stream_data 本身(JSON 串或解析后的对象)
197
+ const pull = source?.pull_data ?? source?.live_core_sdk_data?.pull_data;
198
+ const data = parseJson(pull?.stream_data ?? (typeof source === 'string' || source?.data ? source : null))?.data;
199
+ if (data && typeof data === 'object') {
200
+ const keys = Object.keys(data).sort((a, b) => (QUALITIES.indexOf(a) + 1 || 99) - (QUALITIES.indexOf(b) + 1 || 99));
201
+ for (const q of keys) {
202
+ const main = data[q]?.main ?? {};
203
+ const size = parseJson(main.sdk_params)?.resolution;
204
+ for (const p of PROTOCOLS)
205
+ push(`${q}-${p}`, main[p], data[q], size);
206
+ }
207
+ }
208
+ for (const [q, url] of Object.entries(source?.flv_pull_url ?? {}))
209
+ push(`${q}-flv`, url, source);
210
+ for (const [q, url] of Object.entries(source?.hls_pull_url_map ?? {}))
211
+ push(`${q}-hls`, url, source);
212
+ push('default-hls', source?.hls_pull_url, source);
213
+ push('default-rtmp', source?.rtmp_pull_url, source);
214
+ return out;
215
+ }
216
+ export function gift(g) {
217
+ return n.gift({ id: n.id(g.id), name: String(g.name ?? ''), price: g.diamond_count != null ? { amount: Number(g.diamond_count), currency: 'TIKTOK_DIAMOND' } : null }, g);
218
+ }
219
+ export function rank(v, i) {
220
+ const u = v.user ?? {};
221
+ const r = ref(u.id_str ?? u.id, u.nickname, u.display_id);
222
+ return r ? n.rank({ rank: Number(v.rank ?? i + 1), user: r, score: n.count(v.score) }, v) : null;
223
+ }
224
+ function liveUserRef(u) {
225
+ return u && u.id !== '0' ? ref(u.id, u.nickname, u.display_id) : null;
226
+ }
227
+ /** 直播 protobuf 事件 → Event;只保留上游关心的 chat / like / gift。 */
228
+ export function liveEvent(e) {
229
+ if (e.type === 'chat')
230
+ return n.event({ type: 'chat', user: liveUserRef(e.user), text: e.text ?? '' }, e);
231
+ if (e.type === 'like')
232
+ return n.event({ type: 'like', user: liveUserRef(e.user), text: e.count != null ? String(e.count) : null }, e);
233
+ if (e.type === 'gift')
234
+ return n.event({ type: 'gift', user: liveUserRef(e.user), gift: { name: e.gift?.name || String(e.gift_id ?? ''), count: e.combo_count || 1 } }, e);
235
+ return null;
236
+ }
237
+ export function categories(tabs) {
238
+ return tabs.flatMap((t) => [
239
+ n.category({ id: String(t.tab_type ?? t.tab_name), name: String(t.tab_name ?? t.tab_type) }, t),
240
+ ...(t.sub_tabs ?? []).map((s) => n.category({ id: String(s.tab_type ?? s.tab_name), name: String(s.tab_name ?? s.tab_type), parent_id: String(t.tab_type ?? t.tab_name) }, s)),
241
+ ]);
242
+ }
243
+ // ---------------------------------------------------------------- 通知与私信
244
+ const NOTICE_TYPES = { 31: 'comment', 45: 'comment', 33: 'follow', 41: 'like', 60: 'like', 62: 'like', 43: 'mention', 44: 'mention' };
245
+ /**
246
+ * 通知:动态(group 500,按 follow / digg / comment / at 分)与系统通知(group 661,`system` 为 true)。
247
+ * 模板通知(template_notice)的标题在 notice.title_template.title,正文在 notice.content;动态里的转发通知也用模板。
248
+ */
249
+ export function notice(v, system = false) {
250
+ const template = v.template_notice?.notice;
251
+ const type = system ? 'system' : (NOTICE_TYPES[Number(v.type)] ?? (v.follow ? 'follow' : v.digg ? 'like' : v.comment ? 'comment' : v.at ? 'mention' : 'system'));
252
+ const body = v.follow ?? v.digg ?? v.comment ?? v.at ?? v.system ?? template ?? {};
253
+ const u = body.from_user?.[0] ?? body.user ?? body.from_user ?? v.from_user ?? {};
254
+ const aweme = body.aweme ?? body.comment?.aweme ?? null;
255
+ const text = template ? [template.title_template?.title, template.content].filter(Boolean).join('\n') : (body.content ?? body.comment?.text ?? body.title ?? v.content);
256
+ return n.notice({
257
+ id: n.id(v.nid_str ?? v.nid),
258
+ type,
259
+ user: ref(u.uid ?? u.id, u.nickname, u.unique_id ?? u.uniqueId),
260
+ target: aweme?.aweme_id ? { id: n.id(aweme.aweme_id), url: itemUrl(aweme.author?.unique_id, aweme.aweme_id) } : null,
261
+ text: n.str(text),
262
+ created_at: n.time(v.create_time),
263
+ }, v);
264
+ }
265
+ /** 私信文本消息:拉取回包(PulledMessage)和 WS 推送(ImTextMessage)的字段相同。 */
266
+ export function imMessage(m) {
267
+ return n.message({
268
+ id: m.server_message_id,
269
+ conversation_id: m.conversation_id,
270
+ from: n.userRef({ id: m.sender }),
271
+ type: 'text',
272
+ text: m.text,
273
+ created_at: n.time(m.create_time),
274
+ }, m);
275
+ }
276
+ export function conversation(id, peer, last, raw) {
277
+ return n.conversation({
278
+ id,
279
+ peer: peer ? n.userRef({ id: peer }) : null,
280
+ last_message: last?.text ?? null,
281
+ updated_at: last ? n.time(last.create_time) : null,
282
+ }, raw);
283
+ }
284
+ export function poi(v) {
285
+ return n.poi({ id: n.id(v.poi_id ?? v.id), name: String(v.poi_name ?? v.name ?? ''), address: n.str(v.address ?? v.poi_address ?? v.city_name) }, v);
286
+ }