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,144 @@
1
+ import { decodeImage, loadCv } from '../../../core/image.js';
2
+ /**
3
+ * 滑块缺口定位(上游 utils/captcha.py 的 find_gap_x,cv2 + numpy + Pillow)。
4
+ * 图片解码与 OpenCV 用 core/image.ts(PNG 与 Pillow 逐像素一致,半透明像素不经过预乘)。
5
+ * numpy 的部分照原样用 float32 重写。
6
+ */
7
+ /** 解码验证码图片(RGBA,与 Pillow 的 `convert("RGBA")` 相同)。 */
8
+ async function decode(buf) {
9
+ const img = await decodeImage(buf);
10
+ if (!img)
11
+ throw new Error('无法解码验证码图片');
12
+ return img;
13
+ }
14
+ /** RGBA 的一个矩形区域 → BGR 交错像素(`cv2.cvtColor(rgb, COLOR_RGB2BGR)`)。 */
15
+ function bgr(img, x0 = 0, y0 = 0, w = img.width, h = img.height) {
16
+ const out = new Uint8Array(w * h * 3);
17
+ for (let y = 0; y < h; y++) {
18
+ for (let x = 0; x < w; x++) {
19
+ const s = ((y0 + y) * img.width + x0 + x) * 4;
20
+ const d = (y * w + x) * 3;
21
+ out[d] = img.rgba[s + 2];
22
+ out[d + 1] = img.rgba[s + 1];
23
+ out[d + 2] = img.rgba[s];
24
+ }
25
+ }
26
+ return out;
27
+ }
28
+ /**
29
+ * 在背景图里找缺口的横坐标(原图像素,滑块左边缘该落到的 x,保证落在 `[0, W - w]`)。
30
+ * 用滑块 alpha 裁出实际形状,在背景的边缘图上做带掩码的模板匹配;搜索范围裁掉右边界,
31
+ * 屏蔽滑块自身所在的 `[0, w)`;得分不超过 0.25 时退回到“逐列亮度突变”。
32
+ */
33
+ export async function findGapX(bgPng, cutPng) {
34
+ const cv = await loadCv();
35
+ const bgImg = await decode(bgPng);
36
+ const cut = await decode(cutPng);
37
+ let x0 = Infinity;
38
+ let x1 = -1;
39
+ let y0 = Infinity;
40
+ let y1 = -1;
41
+ for (let y = 0; y < cut.height; y++) {
42
+ for (let x = 0; x < cut.width; x++) {
43
+ if (cut.rgba[(y * cut.width + x) * 4 + 3] > 32) {
44
+ if (x < x0)
45
+ x0 = x;
46
+ if (x > x1)
47
+ x1 = x;
48
+ if (y < y0)
49
+ y0 = y;
50
+ if (y > y1)
51
+ y1 = y;
52
+ }
53
+ }
54
+ }
55
+ if (x1 < 0)
56
+ throw new Error('滑块图 alpha 全空,无法定位');
57
+ const pieceW = x1 - x0 + 1;
58
+ const pieceH = y1 - y0 + 1;
59
+ const maskData = new Uint8Array(pieceW * pieceH);
60
+ for (let y = 0; y < pieceH; y++)
61
+ for (let x = 0; x < pieceW; x++)
62
+ maskData[y * pieceW + x] = cut.rgba[((y0 + y) * cut.width + x0 + x) * 4 + 3] > 32 ? 255 : 0;
63
+ const bgW = bgImg.width;
64
+ const bgH = bgImg.height;
65
+ const maxX = Math.max(0, bgW - pieceW);
66
+ const mats = [];
67
+ const keep = (m) => (mats.push(m), m);
68
+ try {
69
+ const bg = keep(cv.matFromArray(bgH, bgW, cv.CV_8UC3, bgr(bgImg)));
70
+ const piece = keep(cv.matFromArray(pieceH, pieceW, cv.CV_8UC3, bgr(cut, x0, y0, pieceW, pieceH)));
71
+ const mask = keep(cv.matFromArray(pieceH, pieceW, cv.CV_8UC1, maskData));
72
+ // 缺口和滑块的边缘形状一致,用边缘图匹配比灰度稳
73
+ const edge = (src) => {
74
+ const blur = keep(new cv.Mat());
75
+ cv.GaussianBlur(src, blur, new cv.Size(3, 3), 0, 0, cv.BORDER_DEFAULT);
76
+ const out = keep(new cv.Mat());
77
+ cv.Canny(blur, out, 60, 180, 3, false);
78
+ return out;
79
+ };
80
+ const bgEdge = edge(bg);
81
+ const pieceEdge = edge(piece);
82
+ const kernel = keep(cv.Mat.ones(3, 3, cv.CV_8U));
83
+ const eroded = keep(new cv.Mat());
84
+ cv.erode(mask, eroded, kernel, new cv.Point(-1, -1), 1, cv.BORDER_CONSTANT, cv.morphologyDefaultBorderValue());
85
+ const res = keep(new cv.Mat());
86
+ cv.matchTemplate(bgEdge, pieceEdge, res, cv.TM_CCOEFF_NORMED, eroded);
87
+ // np.nan_to_num(-1) 后按列取纵向最大(float32)
88
+ const cols = res.cols;
89
+ const rows = res.rows;
90
+ const scores = res.data32F;
91
+ const byX = new Float32Array(cols);
92
+ for (let c = 0; c < cols; c++) {
93
+ let m = -Infinity;
94
+ for (let r = 0; r < rows; r++) {
95
+ const v = scores[r * cols + c];
96
+ const s = Number.isFinite(v) ? v : -1;
97
+ if (s > m)
98
+ m = s;
99
+ }
100
+ byX[c] = m;
101
+ }
102
+ const valid = new Float32Array(cols).fill(-1);
103
+ const upper = Math.min(maxX, cols - 1);
104
+ for (let c = 0; c <= upper; c++)
105
+ valid[c] = byX[c];
106
+ for (let c = 0; c < Math.min(pieceW, cols); c++)
107
+ valid[c] = -1;
108
+ const bestX = argmax(valid);
109
+ const clamp = (x) => Math.min(Math.max(x, 0), maxX);
110
+ if (valid[bestX] > 0.25)
111
+ return clamp(bestX);
112
+ // 退路:缺口通常比周围暗,逐列找亮度突变(同样只在合法区间里找)
113
+ const gray = keep(new cv.Mat());
114
+ cv.cvtColor(bg, gray, cv.COLOR_BGR2GRAY);
115
+ const g = gray.data;
116
+ const col = new Float32Array(bgW);
117
+ for (let x = 0; x < bgW; x++) {
118
+ let sum = 0;
119
+ for (let y = 0; y < bgH; y++)
120
+ sum = Math.fround(sum + g[y * bgW + x]);
121
+ col[x] = Math.fround(sum / bgH);
122
+ }
123
+ const diff = new Float32Array(Math.max(0, bgW - 1));
124
+ for (let x = 0; x < diff.length; x++)
125
+ diff[x] = Math.abs(Math.fround(col[x + 1] - col[x]));
126
+ const lo = pieceW;
127
+ const hi = Math.min(maxX, diff.length - 1);
128
+ if (hi <= lo)
129
+ return clamp(bestX);
130
+ return clamp(argmax(diff.subarray(lo, hi + 1)) + lo);
131
+ }
132
+ finally {
133
+ for (const m of mats)
134
+ m.delete();
135
+ }
136
+ }
137
+ /** np.argmax:第一个最大值的下标(这里没有 NaN)。 */
138
+ function argmax(a) {
139
+ let best = 0;
140
+ for (let i = 1; i < a.length; i++)
141
+ if (a[i] > a[best])
142
+ best = i;
143
+ return best;
144
+ }
@@ -0,0 +1,376 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { SpecificAIError } from '../../../core/errors.js';
3
+ import { md5Hex } from '../../../core/hash.js';
4
+ import { compactJson, jsonLoads } from '../../../core/py.js';
5
+ import * as rand from '../../../core/rand.js';
6
+ import { ACCEPT_ENCODING, ACCEPT_LANGUAGE, CAPTCHA_HOST, PROFILE, RESOLUTION } from './profile.js';
7
+ /**
8
+ * gdfp manMachine 预检(上游 utils/gdfp_manmachine.py):滑块提交 verify 之前必须先上报的行为遥测。
9
+ * SDK_INIT(换策略,上报路径由它下发)→ core `/n/a/b` → 等策略里的 wait 毫秒 → whole `/n/a/b`。
10
+ * 不做这一步直接提交,服务端回 350014 anti check err。
11
+ *
12
+ * 分辨率、平台、CPU、WebGL 与屏幕尺寸取自统一的浏览器档案 {@link PROFILE},与请求头和 captchaExtraParam 一致。
13
+ *
14
+ * 响应按上游严格校验:状态码、content-type、响应体逐字节。协议版本不查:上游抓包时是 HTTP/2,现在默认也接受
15
+ * HTTP/1.1(有的网络上这个接口只协商出 HTTP/1.1,只认 HTTP/2 会让验证码永远走不到 verify;设
16
+ * KS_STRICT_GDFP_HTTP2=1 才恢复只认 HTTP/2),而 specific-ai 的 HttpResponse 本来就拿不到协商的协议版本。
17
+ */
18
+ export const APP_KEY = '10001001';
19
+ export const SECRET_KEY = 'f2fff381c551a8dcdb765e316f3d44ac';
20
+ const CORE_DATA_LOGGER_ID = '10001002';
21
+ const RM_VERSION = '1.6.0';
22
+ const HOST = 'https://gdfp.gifshow.com';
23
+ const URL_STRATEGY = '/s/u/v';
24
+ export const URL_CORE_REPORT = '/n/a/b';
25
+ const BUSS_TYPE_MAN_MACHINE = 'manMachine';
26
+ const FLAG = 2;
27
+ const RESPONSE_CONTENT_TYPE = 'application/json;charset=UTF-8';
28
+ export const MAN_MACHINE_INIT_RESPONSE = '{"result":1,"error_msg":"","antispamPluginRsp":' +
29
+ '"eyJzd2l0Y2giOjEsIm1heEJhdGNoTGVuZ3RoIjo1MCwid2FpdCI6MTAwMCwiZW5hYmxlTmF0aXZlIjoxLCJqc3ZlciI6IjEuMC4xIiwicmVwb3J0Q29uZmlnIjp7InJlcG9ydFVybHMiOlsiL24vYS9iIl19LCJwb2xpY3lJZCI6MjU0LCJwdmVyIjoiMS4wLjIiLCJzdGF0dXMiOjF9"}';
30
+ export const REPORT_RESPONSE = '{"result":1,"error_msg":""}';
31
+ const MAN_MACHINE_INIT_CONFIG = {
32
+ switch: 1,
33
+ maxBatchLength: 50,
34
+ wait: 1000,
35
+ enableNative: 1,
36
+ jsver: '1.0.1',
37
+ reportConfig: { reportUrls: ['/n/a/b'] },
38
+ policyId: 254,
39
+ pver: '1.0.2',
40
+ status: 1,
41
+ };
42
+ const SCRIPT_BASE = 'https://p23-plat.wskwai.com/kos/nlav111449/technology-platform/static/captcha/js';
43
+ export const CAPTCHA_SCRIPT_URLS = [
44
+ `${SCRIPT_BASE}/weblogger.2e328f42.js`,
45
+ `${SCRIPT_BASE}/chunk-vendors.39300a01.js`,
46
+ `${SCRIPT_BASE}/encrypt.ee7d2a41.js`,
47
+ `${SCRIPT_BASE}/iframe/index.c9ae8c81.js`,
48
+ ];
49
+ const PDF = 'Portable Document Format';
50
+ const PLUGIN_LIST = compactJson(['PDF Viewer', 'Chrome PDF Viewer', 'Chromium PDF Viewer', 'Microsoft Edge PDF Viewer', 'WebKit built-in PDF'].map((name) => ({
51
+ name,
52
+ filename: 'internal-pdf-viewer',
53
+ description: PDF,
54
+ })));
55
+ const MIME_LIST = compactJson([
56
+ { type: 'application/pdf', description: PDF },
57
+ { type: 'text/pdf', description: PDF },
58
+ ]);
59
+ const PERMISSION_STATES = [
60
+ ['speaker', 'error'],
61
+ ['device-info', 'error'],
62
+ ['bluetooth', 'error'],
63
+ ['ambient-light-sensor', 'error'],
64
+ ['clipboard', 'error'],
65
+ ['nfc', 'error'],
66
+ ['geolocation', 'denied'],
67
+ ['notifications', 'denied'],
68
+ ['push', 'denied'],
69
+ ['midi', 'denied'],
70
+ ['camera', 'denied'],
71
+ ['microphone', 'denied'],
72
+ ['background-fetch', 'prompt'],
73
+ ['background-sync', 'granted'],
74
+ ['persistent-storage', 'prompt'],
75
+ ['accelerometer', 'granted'],
76
+ ['gyroscope', 'granted'],
77
+ ['magnetometer', 'granted'],
78
+ ['display-capture', 'denied'],
79
+ ].map(([permissionName, state]) => ({ permissionName, state }));
80
+ const CORE_NATIVE_FINGERPRINT = { cf: 585, ch: '3b4de2be29905d2bcc3812d4fa039bc3', ff: 166, fh: '7daf378bafe0d3babac2696177d5de0d', el: 33, eh: 'dc4d3e06840e103f3e77747a82029aa9' };
81
+ const WHOLE_NATIVE_FINGERPRINT = {
82
+ pc: 134,
83
+ ph: '594d639a69bb0bdf419c1c14ccbb560a',
84
+ wf: 1159,
85
+ wh: '6a9ba63fd2cbda1ce03b5204db881cac',
86
+ cf: 585,
87
+ ch: '3b4de2be29905d2bcc3812d4fa039bc3',
88
+ mf: 1052,
89
+ mh: '3bbcec74126d8f15aa514a6c7b4179f8',
90
+ ff: 166,
91
+ fh: '7daf378bafe0d3babac2696177d5de0d',
92
+ af: 1031,
93
+ ah: '744f127aa70ee2cd33f97f47cf641a77',
94
+ gr: 598,
95
+ gh: '54a77c38470096d9684100f3909f3109',
96
+ el: 33,
97
+ eh: 'dc4d3e06840e103f3e77747a82029aa9',
98
+ ow: 19748,
99
+ oh: '4f323cba12d2aba1b62665eb7b260b40',
100
+ ky: 33,
101
+ kh: '61792d30f54bedfe1007347e9fdc4223',
102
+ };
103
+ const sha1 = (s) => createHash('sha1').update(s, 'utf8').digest('hex');
104
+ /** `md5Hex(appKey + secretKey + 秒级时间戳)`。 */
105
+ export const signFor = (ts) => md5Hex(`${APP_KEY}${SECRET_KEY}${ts}`);
106
+ /** gdfp 请求地址(appkey / seckey / bussType / timestamp / sign)。 */
107
+ export function buildUrl(path, bussType = BUSS_TYPE_MAN_MACHINE, tsSeconds, extra = '') {
108
+ const ts = tsSeconds ?? rand.nowSeconds();
109
+ return `${HOST}${path}?appkey=${APP_KEY}&seckey=${SECRET_KEY}&bussType=${bussType}&timestamp=${ts}&sign=${signFor(ts)}${extra}`;
110
+ }
111
+ function scriptList(scriptUrls = []) {
112
+ const out = [...CAPTCHA_SCRIPT_URLS];
113
+ for (const url of scriptUrls)
114
+ if (url && !out.includes(url))
115
+ out.push(url);
116
+ return out;
117
+ }
118
+ function cookieFingerprint(cookies) {
119
+ const line = cookies.map(([k, v]) => `${k}=${v}`).join('; ');
120
+ return { ci: [...line].length, ih: md5Hex(line) };
121
+ }
122
+ function callStack(kind) {
123
+ const base = CAPTCHA_SCRIPT_URLS[0];
124
+ const frames = kind === 'whole'
125
+ ? [
126
+ ['printCallStack', 34236],
127
+ ['RiskMgt._this.generateModuleSection', 77458],
128
+ ['RiskMgt.<anonymous>', 84095],
129
+ ['', 4825],
130
+ ['Object.next', 4930],
131
+ ['', 3857],
132
+ ['newPromise(<anonymous>)', null],
133
+ ['__awaiter', 3602],
134
+ ['RiskMgt.sendWholeData', 83975],
135
+ ['RiskMgt.<anonymous>', 83076],
136
+ ]
137
+ : [
138
+ ['printCallStack', 34236],
139
+ ['RiskMgt._this.generateCoreModuleSection', 79648],
140
+ ['RiskMgt.<anonymous>', 83773],
141
+ ['', 4825],
142
+ ['Object.next', 4930],
143
+ ['', 3857],
144
+ ['newPromise(<anonymous>)', null],
145
+ ['__awaiter', 3602],
146
+ ['RiskMgt.sendCoreData', 83346],
147
+ ['n', 82235],
148
+ ];
149
+ return frames.map(([name, offset]) => (offset == null ? `at${name}` : name ? `at${name}(${base}:1:${offset})` : `at${base}:1:${offset}`)).join('');
150
+ }
151
+ function timings(begin, now, whole = false) {
152
+ const values = { cs: begin + 246, ce: begin + 250, as: begin + 255, ae: begin + 259, cae: begin + 338, ls: now };
153
+ if (whole)
154
+ Object.assign(values, { le: begin + 472, cre: begin + 688, gs: begin + 492, ge: begin + 515 });
155
+ return values;
156
+ }
157
+ const afterMozilla = (ua) => (ua.includes('Mozilla/') ? ua.slice(ua.indexOf('Mozilla/') + 'Mozilla/'.length) : ua);
158
+ /**
159
+ * `generateCoreModuleSection` 的产物(`/n/a/b` 第一次上报)。
160
+ * 字段号都是递增的,普通对象里形如整数的键按数值升序输出,与上游的插入顺序一致。
161
+ */
162
+ export function buildCorePayload(o) {
163
+ const resolution = o.resolution || RESOLUTION;
164
+ const section = {
165
+ 1: { page: o.parentUrl, identity: o.identity, page_type: 2 },
166
+ 2: o.iframeUrl,
167
+ 5: o.sessionId,
168
+ 7: Object.fromEntries(o.cookies),
169
+ 8: resolution,
170
+ 11: o.ua,
171
+ 17: o.did,
172
+ 21: { type: 'PAGE_ENTER', timestamp: o.nowMs - 20, initTime: o.beginMs },
173
+ 28: sha1(`${o.did}${o.parentUrl}${resolution}${o.ua}${o.iframeUrl}`),
174
+ 33: 'Mozilla',
175
+ 34: 'Netscape',
176
+ 35: afterMozilla(o.ua),
177
+ 36: PROFILE.platform,
178
+ 55: 0,
179
+ 56: o.ua,
180
+ 69: '0043c0b8a0b002e8133a140d14068859',
181
+ 75: '1',
182
+ 76: '',
183
+ 77: scriptList(o.scriptUrls),
184
+ 79: callStack('core'),
185
+ 82: { ...CORE_NATIVE_FINGERPRINT, ...cookieFingerprint(o.cookies) },
186
+ 89: timings(o.beginMs, o.nowMs),
187
+ 102: 50,
188
+ 108: {},
189
+ };
190
+ return { 1: o.did, 3: CORE_DATA_LOGGER_ID, 6: BUSS_TYPE_MAN_MACHINE, 7: 10, 9: o.nowMs, 10: '', 12: APP_KEY, 13: '', 14: RM_VERSION, module_section: [section] };
191
+ }
192
+ const LETTERS_DIGITS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
193
+ /** _webrtc_fields:每个 iframe 现生成的 mDNS 主机名、端口、ICE 凭据、证书指纹与 SDP。 */
194
+ export function webrtcFields() {
195
+ const mdns1 = `${rand.uuid4()}.local`;
196
+ const mdns2 = `${rand.uuid4()}.local`;
197
+ const port1 = rand.randint(50000, 59998);
198
+ const port2 = rand.randint(50000, 59998);
199
+ const ufrag = rand.string(4, LETTERS_DIGITS);
200
+ const pwd = rand.string(24, LETTERS_DIGITS + '+/');
201
+ const fingerprint = Array.from({ length: 32 }, () => Math.floor(rand.random() * 256)
202
+ .toString(16)
203
+ .toUpperCase()
204
+ .padStart(2, '0')).join(':');
205
+ // randint(10**18, 10**19 - 1):超出 double 的整数范围,用 BigInt 做加法
206
+ const session = (10n ** 18n + BigInt(Math.floor(rand.random() * 9e18))).toString();
207
+ const candidates = `;candidate:1801140675 1 udp 2113937151 ${mdns1} ${port1} typ host generation 0 ufrag ${ufrag} network-cost 999` +
208
+ `;candidate:1070259456 1 udp 2113939711 ${mdns2} ${port2} typ host generation 0 ufrag ${ufrag} network-cost 999`;
209
+ const candidate1 = `a=candidate:1801140675 1 udp 2113937151 ${mdns1} ${port1} typ host generation 0 network-cost 999\r\n`;
210
+ const candidate2 = `a=candidate:1070259456 1 udp 2113939711 ${mdns2} ${port2} typ host generation 0 network-cost 999\r\n`;
211
+ const prefix = `v=0\r\no=- ${session} 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\n` +
212
+ 'a=group:BUNDLE 0\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS\r\n' +
213
+ 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n' +
214
+ 'c=IN IP4 0.0.0.0\r\n';
215
+ const suffix = `a=ice-ufrag:${ufrag}\r\na=ice-pwd:${pwd}\r\n` +
216
+ `a=ice-options:trickle\r\na=fingerprint:sha-256 ${fingerprint}\r\n` +
217
+ 'a=setup:actpass\r\na=mid:0\r\na=sctp-port:5000\r\na=max-message-size:262144\r\n';
218
+ // Chrome 的 promise 先在第一个 host candidate 时 resolve,第二个收集完再来一次:两段 `;v=0`
219
+ const sdp = `;${prefix}${candidate1}${suffix};${prefix}${candidate1}${candidate2}${suffix}`;
220
+ return [candidates, sdp];
221
+ }
222
+ /** `sendWholeData` 的完整上报(第二次 `/n/a/b`)。 */
223
+ export function buildWholePayload(o) {
224
+ const resolution = o.resolution || RESOLUTION;
225
+ const [candidates, sdp] = webrtcFields();
226
+ const section = {
227
+ 1: { page: o.parentUrl, identity: o.identity, page_type: 2 },
228
+ 2: o.iframeUrl,
229
+ 4: CAPTCHA_HOST,
230
+ 5: o.sessionId,
231
+ 6: 'captcha',
232
+ 7: Object.fromEntries(o.cookies),
233
+ 8: resolution,
234
+ 9: 360,
235
+ 10: 360,
236
+ 11: o.ua,
237
+ 12: 'NT 10.0',
238
+ 13: 'zh-CN',
239
+ 14: 'Windows',
240
+ 17: o.did,
241
+ 21: { type: 'PAGE_ENTER', timestamp: o.beginMs + 491, initTime: o.beginMs },
242
+ 24: '10',
243
+ 25: '10-1',
244
+ 26: '0',
245
+ 28: sha1(`${o.did}${o.parentUrl}${resolution}${o.ua}${o.iframeUrl}`),
246
+ 29: o.reportPath ?? URL_CORE_REPORT,
247
+ 30: 'production',
248
+ 31: 1,
249
+ 32: '0.00',
250
+ 33: 'Mozilla',
251
+ 34: 'Netscape',
252
+ 35: afterMozilla(o.ua),
253
+ 36: PROFILE.platform,
254
+ 37: '["zh-CN","zh","en","zh-TW","ja"]',
255
+ 41: '20030107',
256
+ 42: 'Google Inc.',
257
+ 43: '',
258
+ 44: PROFILE.cpuCores,
259
+ 50: 10,
260
+ 51: '',
261
+ 52: 1,
262
+ 53: 'Gecko',
263
+ 54: true,
264
+ 55: 0,
265
+ 56: o.ua,
266
+ 57: 'zh-CN',
267
+ 58: PLUGIN_LIST,
268
+ 59: MIME_LIST,
269
+ 61: PROFILE.webglVendor,
270
+ 62: PROFILE.webglRenderer,
271
+ 63: '1',
272
+ 64: 'WebKit',
273
+ 65: 'WebGL 1.0 (OpenGL ES 2.0 Chromium)',
274
+ 66: 'WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)',
275
+ 67: 0,
276
+ 68: 'f3bd24f5b153d57f5817372fd5f22573',
277
+ 69: '0043c0b8a0b002e8133a140d14068859',
278
+ 70: 'ba6689f9a1550fb5eef25d1fc682c8c1',
279
+ 75: '1',
280
+ 76: '',
281
+ 77: scriptList(o.scriptUrls),
282
+ 78: { w: { pc: 1256, kc: 255, lc: 261 }, n: { pc: 0, kc: 0, lc: 82 } },
283
+ 79: callStack('whole'),
284
+ 80: '2200',
285
+ 81: '1032',
286
+ 82: { ...WHOLE_NATIVE_FINGERPRINT, ...cookieFingerprint(o.cookies) },
287
+ 85: candidates,
288
+ 86: sdp,
289
+ 87: { w: PROFILE.screenWidth, h: PROFILE.screenHeight, c: 24, p: 24 },
290
+ 88: md5Hex(sdp),
291
+ 89: timings(o.beginMs, o.nowMs, true),
292
+ 90: '',
293
+ 100: 11,
294
+ 101: { lsc: 10, ssc: 2 },
295
+ 102: 50,
296
+ 103: { en: false, isF: false },
297
+ 104: { ts: o.beginMs + 520, cts: o.beginMs + 520 },
298
+ 105: '0',
299
+ 106: PERMISSION_STATES.map((p) => ({ ...p })),
300
+ 107: -8,
301
+ 108: {},
302
+ };
303
+ return { 1: o.did, 3: APP_KEY, 6: BUSS_TYPE_MAN_MACHINE, 7: 10, 9: o.nowMs, 10: '', 12: APP_KEY, 13: '', 14: RM_VERSION, module_section: [section] };
304
+ }
305
+ /** 外层封装:`{"flag":2,"data":"<base64(JSON)>"}`。 */
306
+ export function encodeBody(payload) {
307
+ return compactJson({ flag: FLAG, data: Buffer.from(compactJson(payload), 'utf8').toString('base64') });
308
+ }
309
+ /** `/s/u/v?type=SDK_INIT` 的 body:`{"data":"<base64>","flag":2}`(发空 body 会被回 PARAM_INVALID)。 */
310
+ export function buildInitBody(did, sdkVer = RM_VERSION, pver = '0.0.0', platform = 3, bussType = BUSS_TYPE_MAN_MACHINE) {
311
+ const inner = compactJson({ device_id: did, sdkver: sdkVer, pver, hp: bussType, platform });
312
+ return compactJson({ data: Buffer.from(inner, 'utf8').toString('base64'), flag: FLAG });
313
+ }
314
+ /** 解 SDK_INIT 响应里的 `antispamPluginRsp`(base64 的 JSON);`/n/a/b` 这个上报路径就是从这里下发的。 */
315
+ export function parseInitResponse(resp) {
316
+ const blob = String(resp?.antispamPluginRsp ?? '');
317
+ if (!blob)
318
+ return {};
319
+ try {
320
+ return jsonLoads(Buffer.from(blob, 'base64').toString('utf8'));
321
+ }
322
+ catch {
323
+ return {};
324
+ }
325
+ }
326
+ async function validate(res, role, expected) {
327
+ const fail = (why) => new SpecificAIError('UPSTREAM', `gdfp manMachine ${role} ${why}`, { detail: { kind: 'captcha', status: res.status } });
328
+ if (res.status !== 200)
329
+ throw fail(`HTTP 状态不是 200:${res.status}`);
330
+ const type = res.headers.get('content-type') ?? '';
331
+ if (type !== RESPONSE_CONTENT_TYPE)
332
+ throw fail(`content-type 偏离:${type}`);
333
+ const raw = await res.text();
334
+ if (raw !== expected)
335
+ throw fail(`响应体偏离:${raw.slice(0, 200)}`);
336
+ return jsonLoads(raw);
337
+ }
338
+ /** 完整的 manMachine 预检:SDK_INIT → core /n/a/b → whole /n/a/b。 */
339
+ export async function report(ks, o) {
340
+ const identity = o.identity || rand.uuid4();
341
+ const sessionId = rand.uuid4();
342
+ const beginMs = rand.now();
343
+ const headers = [
344
+ ['user-agent', o.ua],
345
+ ['content-type', 'text/plain;charset=UTF-8'],
346
+ ['referer', o.iframeUrl],
347
+ ['accept', '*/*'],
348
+ ['accept-encoding', ACCEPT_ENCODING],
349
+ ['accept-language', ACCEPT_LANGUAGE],
350
+ ['origin', CAPTCHA_HOST],
351
+ ['priority', 'u=1, i'],
352
+ ['sec-fetch-dest', 'empty'],
353
+ ['sec-fetch-mode', 'cors'],
354
+ ['sec-fetch-site', 'cross-site'],
355
+ ];
356
+ const post = (url, body) => ks.http.request({ method: 'POST', url, headers, body, cookies: false, timeout: 30 });
357
+ // 1) SDK_INIT:换策略,上报路径由它下发
358
+ const init = await validate(await post(buildUrl(URL_STRATEGY, BUSS_TYPE_MAN_MACHINE, undefined, '&type=SDK_INIT'), buildInitBody(o.did)), 'SDK_INIT', MAN_MACHINE_INIT_RESPONSE);
359
+ if (Object.keys(init).join() !== 'result,error_msg,antispamPluginRsp' || init.result !== 1 || init.error_msg !== '') {
360
+ throw new SpecificAIError('UPSTREAM', 'gdfp manMachine SDK_INIT 响应偏离', { detail: { kind: 'captcha' } });
361
+ }
362
+ const config = parseInitResponse(init);
363
+ if (compactJson(config) !== compactJson(MAN_MACHINE_INIT_CONFIG)) {
364
+ throw new SpecificAIError('UPSTREAM', 'gdfp manMachine 策略偏离,拒绝继续', { detail: { kind: 'captcha', config } });
365
+ }
366
+ const path = String(config.reportConfig.reportUrls[0]);
367
+ // 2) 行为遥测
368
+ const common = { ...o, identity, beginMs, sessionId };
369
+ const corePayload = buildCorePayload({ ...common, nowMs: rand.now() });
370
+ const core = await validate(await post(buildUrl(path), encodeBody(corePayload)), 'core report', REPORT_RESPONSE);
371
+ // 3) SDK 策略里的 wait 隔开 core 与 whole 两次上报
372
+ await rand.sleep(Number(config.wait));
373
+ const wholePayload = buildWholePayload({ ...common, nowMs: rand.now(), reportPath: path });
374
+ const whole = await validate(await post(buildUrl(path), encodeBody(wholePayload)), 'whole report', REPORT_RESPONSE);
375
+ return { identity, init, config, core, whole };
376
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * GraphQL query 文本(上游 ks_apis/kuaishou_api.py 逐字复制:服务端按 query 文本做持久化校验,一个字符都不能改)。
3
+ */
4
+ export const SHORT_VIDEO_RECO = "fragment photoContent on PhotoEntity {\n __typename\n id\n duration\n caption\n originCaption\n likeCount\n viewCount\n commentCount\n realLikeCount\n coverUrl\n photoUrl\n photoH265Url\n manifest\n manifestH265\n videoResource\n coverUrls {\n url\n __typename\n }\n timestamp\n expTag\n animatedCoverUrl\n distance\n disableSensitivePhoto\n videoRatio\n liked\n stereoType\n profileUserTopPhoto\n musicBlocked\n riskTagContent\n riskTagUrl\n}\n\nquery visionShortVideoReco($semKeyword: String, $semCrowd: String, $utmSource: String, $utmMedium: String, $page: String, $photoId: String, $utmCampaign: String) {\n visionShortVideoReco(semKeyword: $semKeyword, semCrowd: $semCrowd, utmSource: $utmSource, utmMedium: $utmMedium, page: $page, photoId: $photoId, utmCampaign: $utmCampaign) {\n llsid\n feeds {\n type\n author {\n id\n name\n following\n headerUrl\n __typename\n }\n photo {\n ...photoContent\n __typename\n }\n tags {\n type\n name\n __typename\n }\n authorStatement {\n content\n type\n riskStyleType\n __typename\n }\n canAddComment\n __typename\n }\n __typename\n }\n}\n";
5
+ export const COMMENT_LIST = "query commentListQuery($photoId: String, $pcursor: String) {\n visionCommentList(photoId: $photoId, pcursor: $pcursor) {\n commentCount\n commentCountV2\n pcursor\n rootCommentsV2 {\n commentId\n authorId\n authorName\n content\n headurl\n timestamp\n hasSubComments\n likedCount\n liked\n status\n __typename\n }\n pcursorV2\n rootComments {\n commentId\n authorId\n authorName\n content\n headurl\n timestamp\n likedCount\n realLikedCount\n liked\n status\n authorLiked\n subCommentCount\n subCommentsPcursor\n subComments {\n commentId\n authorId\n authorName\n content\n headurl\n timestamp\n likedCount\n realLikedCount\n liked\n status\n authorLiked\n replyToUserName\n replyTo\n __typename\n }\n __typename\n }\n __typename\n }\n}\n";
6
+ export const SUB_COMMENT_LIST = "mutation visionSubCommentList($photoId: String, $rootCommentId: String, $pcursor: String) {\n visionSubCommentList(photoId: $photoId, rootCommentId: $rootCommentId, pcursor: $pcursor) {\n pcursor\n subComments {\n commentId\n authorId\n authorName\n content\n headurl\n timestamp\n likedCount\n realLikedCount\n liked\n status\n authorLiked\n replyToUserName\n replyTo\n __typename\n }\n pcursorV2\n subCommentsV2 {\n commentId\n authorId\n authorName\n content\n headurl\n timestamp\n hasSubComments\n likedCount\n liked\n status\n replyToUserName\n replyTo\n __typename\n }\n __typename\n }\n}\n";
7
+ export const VIDEO_DETAIL = "query visionVideoDetail($photoId: String, $type: String, $page: String, $webPageArea: String) {\n visionVideoDetail(photoId: $photoId, type: $type, page: $page, webPageArea: $webPageArea) {\n status\n type\n author {\n id\n name\n following\n headerUrl\n livingInfo\n verifiedDetail\n __typename\n }\n photo {\n id\n duration\n caption\n likeCount\n realLikeCount\n coverUrl\n photoUrl\n liked\n timestamp\n expTag\n llsid\n viewCount\n videoRatio\n stereoType\n musicBlocked\n riskTagContent\n riskTagUrl\n manifest {\n mediaType\n businessType\n version\n adaptationSet {\n id\n duration\n representation {\n id\n defaultSelect\n backupUrl\n codecs\n url\n height\n width\n avgBitrate\n maxBitrate\n m3u8Slice\n qualityType\n qualityLabel\n frameRate\n featureP2sp\n hidden\n disableAdaptive\n __typename\n }\n __typename\n }\n __typename\n }\n manifestH265\n photoH265Url\n coronaCropManifest\n coronaCropManifestH265\n croppedPhotoH265Url\n croppedPhotoUrl\n videoResource\n __typename\n }\n authorStatement {\n content\n type\n riskStyleType\n __typename\n }\n tags {\n type\n name\n __typename\n }\n commentLimit {\n canAddComment\n __typename\n }\n llsid\n danmakuSwitch\n __typename\n }\n}\n";
8
+ export const USER_INFO = "query userInfoQuery {\n userInfo {\n id\n name\n avatar\n eid\n userId\n __typename\n }\n}\n";
9
+ export const LIKE_DATA = "fragment photoContent on PhotoEntity {\n __typename\n id\n duration\n caption\n originCaption\n likeCount\n viewCount\n commentCount\n realLikeCount\n coverUrl\n photoUrl\n photoH265Url\n manifest\n manifestH265\n videoResource\n coverUrls {\n url\n __typename\n }\n timestamp\n expTag\n animatedCoverUrl\n distance\n disableSensitivePhoto\n videoRatio\n liked\n stereoType\n profileUserTopPhoto\n musicBlocked\n riskTagContent\n riskTagUrl\n}\n\nfragment recoPhotoFragment on recoPhotoEntity {\n __typename\n id\n duration\n caption\n originCaption\n likeCount\n viewCount\n commentCount\n realLikeCount\n coverUrl\n photoUrl\n photoH265Url\n manifest\n manifestH265\n videoResource\n coverUrls {\n url\n __typename\n }\n timestamp\n expTag\n animatedCoverUrl\n distance\n videoRatio\n liked\n stereoType\n profileUserTopPhoto\n musicBlocked\n riskTagContent\n riskTagUrl\n}\n\nfragment feedContentWithLiveInfo on Feed {\n type\n author {\n id\n name\n headerUrl\n following\n livingInfo\n headerUrls {\n url\n __typename\n }\n verifiedDetail {\n description\n iconType\n newVerified\n musicCompany\n type\n __typename\n }\n __typename\n }\n photo {\n ...photoContent\n ...recoPhotoFragment\n __typename\n }\n canAddComment\n llsid\n status\n authorStatement {\n content\n type\n riskStyleType\n __typename\n }\n currentPcursor\n tags {\n type\n name\n __typename\n }\n __typename\n}\n\nfragment photoResult on PhotoResult {\n result\n llsid\n expTag\n serverExpTag\n pcursor\n feeds {\n ...feedContentWithLiveInfo\n __typename\n }\n webPageArea\n __typename\n}\n\nquery likeDataQuery($pcursor: String, $page: String, $token: String, $st: Long) {\n likeData(pcursor: $pcursor, page: $page, token: $token, st: $st) {\n ...photoResult\n __typename\n }\n}\n";
10
+ export const VISION_PROFILE_REDUCED = "query visionProfileReduced($userId: String) {\n visionProfileReduced(userId: $userId) {\n result\n hostName\n userProfile {\n profile {\n gender\n user_name\n user_id\n headurl\n user_text\n user_profile_bg_url\n __typename\n }\n isFollowing\n isUserIsolated\n livingInfo\n __typename\n }\n __typename\n }\n}\n";
11
+ export const CHECK_LOGIN = "query checkLoginQuery {\n checkLogin\n}\n";
@@ -0,0 +1,125 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { createDecipheriv } from 'node:crypto';
3
+ import { gunzipSync, inflateRawSync } from 'node:zlib';
4
+ import protobuf from 'protobufjs';
5
+ import * as n from '../../../core/normalize.js';
6
+ import { staticFile } from '../../../core/paths.js';
7
+ /**
8
+ * 直播弹幕 WebSocket 的 protobuf 编解码(上游 utils/live_proto.py)。schema 是 live-app.js 内嵌的
9
+ * protobufjs descriptor(static/kuaishou/fixtures/live_ws_proto.json),直接交给 Root.fromJSON。
10
+ *
11
+ * 信封 SocketMessage { payloadType=1, compressionType=2, payload=3 };payload 按 compressionType
12
+ * 处理:3 → AES-128-CBC 解密,2 → gzip,其余原样;再按 payloadType 对应的消息类型解码。
13
+ */
14
+ const WS_AES_KEY = Buffer.from('PPbzKKL7NB15leYy');
15
+ const WS_AES_IV = Buffer.from('JRODKJiolJ9xqso0');
16
+ let cached = null;
17
+ function schema() {
18
+ if (cached)
19
+ return cached;
20
+ // 上游抽出的 descriptor 里 CommentImageSegment 有个拼写错误的类型 `unit32`(上游 Python 解码器不校验类型,
21
+ // protobufjs 会拒绝);静态文件保持原样,这里在内存里改成 uint32
22
+ const descriptor = JSON.parse(readFileSync(staticFile('kuaishou', 'fixtures/live_ws_proto.json'), 'utf8').replaceAll('"type": "unit32"', '"type": "uint32"').replaceAll('"type":"unit32"', '"type":"uint32"'));
23
+ const maps = JSON.parse(readFileSync(staticFile('kuaishou', 'fixtures/live_ws_maps.json'), 'utf8'));
24
+ const root = protobuf.Root.fromJSON({ nested: descriptor });
25
+ const types = new Map();
26
+ const enums = new Map();
27
+ // 与上游一样按简单名字索引:先深后浅,根上的同名类型覆盖嵌套的
28
+ const walk = (ns, depth, out) => {
29
+ for (const obj of ns.nestedArray) {
30
+ out.push([depth, obj]);
31
+ if (obj instanceof protobuf.Namespace)
32
+ walk(obj, depth + 1, out);
33
+ }
34
+ };
35
+ const all = [];
36
+ walk(root, 0, all);
37
+ all.sort((a, b) => b[0] - a[0]);
38
+ for (const [, obj] of all) {
39
+ if (obj instanceof protobuf.Type)
40
+ types.set(obj.name, obj);
41
+ else if (obj instanceof protobuf.Enum)
42
+ enums.set(obj.name, obj);
43
+ }
44
+ cached = { root, types, enums, numberToName: maps.number_to_name, nameToType: maps.name_to_type, upstream: maps.upstream };
45
+ return cached;
46
+ }
47
+ function type(name) {
48
+ const t = schema().types.get(name);
49
+ if (!t)
50
+ throw new Error(`live proto 没有类型 ${name}`);
51
+ return t;
52
+ }
53
+ /** 编一帧上行:CS_ENTER_ROOM / CS_HEARTBEAT / CS_USER_EXIT。 */
54
+ export function encodeFrame(kind, payload) {
55
+ const [number, typeName] = schema().upstream[kind];
56
+ const body = type(typeName).encode(type(typeName).fromObject(payload)).finish();
57
+ return type('SocketMessage').encode({ payloadType: number, payload: body }).finish();
58
+ }
59
+ /** 进房帧:连上 WebSocket 后第一件事,token 来自 liveroom/websocketinfo。 */
60
+ export function enterRoomFrame(token, liveStreamId, reconnectCount = 0) {
61
+ const payload = { token, liveStreamId };
62
+ if (reconnectCount)
63
+ payload.reconnectCount = reconnectCount;
64
+ return encodeFrame('CS_ENTER_ROOM', payload);
65
+ }
66
+ export const heartbeatFrame = (timestampMs) => encodeFrame('CS_HEARTBEAT', { timestamp: timestampMs });
67
+ export const userExitFrame = (timeMs) => encodeFrame('CS_USER_EXIT', { time: timeMs });
68
+ function decompress(payload, compression) {
69
+ if (compression === 3) {
70
+ const d = createDecipheriv('aes-128-cbc', WS_AES_KEY, WS_AES_IV);
71
+ d.setAutoPadding(false);
72
+ let plain = Buffer.concat([d.update(payload), d.final()]);
73
+ const pad = plain[plain.length - 1] ?? 0;
74
+ if (pad >= 1 && pad <= 16 && plain.subarray(plain.length - pad).every((b) => b === pad))
75
+ plain = plain.subarray(0, plain.length - pad);
76
+ return plain;
77
+ }
78
+ if (compression === 2) {
79
+ try {
80
+ return gunzipSync(payload);
81
+ }
82
+ catch {
83
+ return inflateRawSync(payload);
84
+ }
85
+ }
86
+ return Buffer.from(payload);
87
+ }
88
+ /** 解一帧下行。不在 bundle h 表里的类型浏览器会直接丢弃,这里保留原始字节。 */
89
+ export function decodeFrame(frame) {
90
+ const s = schema();
91
+ const env = type('SocketMessage').decode(frame);
92
+ const number = Number(env.payloadType ?? 0);
93
+ const name = s.numberToName[String(number)] ?? s.enums.get('PayloadType')?.valuesById[number] ?? String(number);
94
+ let payload = {};
95
+ if (env.payload?.length) {
96
+ const raw = decompress(env.payload, Number(env.compressionType ?? 0));
97
+ const typeName = s.nameToType[name];
98
+ payload = typeName && s.types.has(typeName) ? type(typeName).toObject(type(typeName).decode(raw), { longs: String, enums: String, defaults: false }) : { _raw: raw.toString('hex') };
99
+ }
100
+ return { type: name, payloadType: number, payload };
101
+ }
102
+ function liveUser(u) {
103
+ if (!u?.principalId)
104
+ return null;
105
+ return n.userRef({ id: u.principalId, name: u.userName, url: `https://live.kuaishou.com/profile/${u.principalId}` });
106
+ }
107
+ /** SC_FEED_PUSH → Event:弹幕、点赞、礼物、分享与系统通知。 */
108
+ export function feedEvents(payload, giftName) {
109
+ const out = [];
110
+ const at = (v) => n.time(v) ?? undefined;
111
+ for (const f of payload?.commentFeeds ?? [])
112
+ out.push(n.event({ type: 'chat', time: at(f.time), user: liveUser(f.user), text: String(f.content ?? '') }, f));
113
+ for (const f of payload?.likeFeeds ?? [])
114
+ out.push(n.event({ type: 'like', user: liveUser(f.user) }, f));
115
+ for (const f of payload?.giftFeeds ?? []) {
116
+ const id = String(f.giftId ?? '');
117
+ const count = Number(f.batchSize || f.comboCount || 1);
118
+ out.push(n.event({ type: 'gift', time: at(f.time), user: liveUser(f.user), gift: { name: giftName(id) ?? id, count } }, f));
119
+ }
120
+ for (const f of payload?.shareFeeds ?? [])
121
+ out.push(n.event({ type: 'other', time: at(f.time), user: liveUser(f.user), text: '分享了直播间' }, f));
122
+ for (const f of payload?.systemNoticeFeeds ?? [])
123
+ out.push(n.event({ type: 'other', time: at(f.time), user: liveUser(f.user), text: n.str(f.content) }, f));
124
+ return out;
125
+ }