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,232 @@
1
+ import { SpecificAIError } from '../../../core/errors.js';
2
+ import { classify, crop, decodeImage, detect } from './ddddocr.js';
3
+ /**
4
+ * 极验点选识别(上游 utils/geetest_ocr.py、geetest_metric.py、geetest_hybrid.py):
5
+ * 1. 题图切成拼图(上面)和提示条(底部 40px),ddddocr 检测模型分别找字框,合并同一个字的重叠框;
6
+ * 2. 每个字框抠出来过 ddddocr 识别模型,取 CTC 各时间步的字符概率最大值作为向量(不需要认对字);
7
+ * 3. 提示字与候选字两两算余弦相似度,匈牙利算法求一一对应,按提示条从左到右的顺序输出点击坐标。
8
+ */
9
+ export const HINT_HEIGHT = 40;
10
+ const PAD = 4;
11
+ /** 切成拼图和提示条(split_sprite)。 */
12
+ export function splitSprite(sprite) {
13
+ const { width, height } = sprite;
14
+ return { puzzle: crop(sprite, 0, 0, width, height - HINT_HEIGHT), hint: crop(sprite, 0, height - HINT_HEIGHT, width, height) };
15
+ }
16
+ /** 按 x1 从左到右,x1 相同时保持原顺序(Python 的 sorted 是稳定排序)。 */
17
+ const byX = (list) => list
18
+ .map((b, i) => [b, i])
19
+ .sort((a, b) => a[0][0] - b[0][0] || a[1] - b[1])
20
+ .map(([b]) => b);
21
+ /** 检测字符框,按 x1 从左到右(detect_boxes)。 */
22
+ export async function detectBoxes(im) {
23
+ return byX((await detect(im)));
24
+ }
25
+ function intersectionRatio(a, b) {
26
+ const inter = Math.max(0, Math.min(a[2], b[2]) - Math.max(a[0], b[0])) * Math.max(0, Math.min(a[3], b[3]) - Math.max(a[1], b[1]));
27
+ const areaA = Math.max(1, (a[2] - a[0]) * (a[3] - a[1]));
28
+ const areaB = Math.max(1, (b[2] - b[0]) * (b[3] - b[1]));
29
+ return inter / Math.min(areaA, areaB);
30
+ }
31
+ /** 合并同一个彩色字的重叠框(merge_duplicate_boxes):交集占较小框的比例 ≥ threshold 的连成一组取并集。 */
32
+ export function mergeDuplicateBoxes(boxes, threshold = 0.25) {
33
+ const groups = [];
34
+ for (const box of byX(boxes.map((b) => b.map((v) => Math.trunc(v))))) {
35
+ const hits = groups.map((g, i) => (g.some((old) => intersectionRatio(box, old) >= threshold) ? i : -1)).filter((i) => i >= 0);
36
+ if (!hits.length) {
37
+ groups.push([box]);
38
+ continue;
39
+ }
40
+ const target = groups[hits[0]];
41
+ target.push(box);
42
+ for (const index of hits.slice(1).reverse())
43
+ target.push(...groups.splice(index, 1)[0]);
44
+ }
45
+ return byX(groups.map((g) => [Math.min(...g.map((b) => b[0])), Math.min(...g.map((b) => b[1])), Math.max(...g.map((b) => b[2])), Math.max(...g.map((b) => b[3]))]));
46
+ }
47
+ /** 提示条相邻字框重叠时,从两框交界的中点切开(_split_overlaps)。 */
48
+ export function splitOverlaps(boxes) {
49
+ const fixed = boxes.map((b) => [...b]);
50
+ for (let i = 0; i < fixed.length - 1; i++) {
51
+ const right = fixed[i][2];
52
+ const left = fixed[i + 1][0];
53
+ if (right > left) {
54
+ const mid = Math.floor((right + left) / 2);
55
+ fixed[i][2] = mid;
56
+ fixed[i + 1][0] = mid;
57
+ }
58
+ }
59
+ return fixed;
60
+ }
61
+ function cropPad(im, [x1, y1, x2, y2], pad) {
62
+ return crop(im, Math.max(0, x1 - pad), Math.max(0, y1 - pad), Math.min(im.width, x2 + pad), Math.min(im.height, y2 + pad));
63
+ }
64
+ /** CTC 概率压成单位向量:各时间步取最大值,去掉 blank(下标 0),再归一化(probability_vector)。 */
65
+ export function probabilityVector(probabilities, steps, classes) {
66
+ const v = new Float64Array(classes);
67
+ for (let t = 0; t < steps; t++)
68
+ for (let c = 0; c < classes; c++)
69
+ v[c] = Math.max(v[c], probabilities[t * classes + c]);
70
+ v[0] = 0;
71
+ let norm = 0;
72
+ for (const x of v)
73
+ norm += x * x;
74
+ norm = Math.sqrt(norm) + 1e-9;
75
+ for (let c = 0; c < classes; c++)
76
+ v[c] /= norm;
77
+ return v;
78
+ }
79
+ async function signature(im) {
80
+ const r = await classify(im);
81
+ return { text: r.text, vector: probabilityVector(r.probabilities, r.steps, r.classes) };
82
+ }
83
+ /**
84
+ * 矩形指派问题(scipy.optimize.linear_sum_assignment,最小化),照抄 scipy 的 rectangular_lsap.cpp
85
+ * (Crouse 的最短增广路),平局时的取法也一致。返回 [行下标, 列下标]。
86
+ */
87
+ export function linearSumAssignment(cost) {
88
+ let nr = cost.length;
89
+ let nc = nr ? cost[0].length : 0;
90
+ if (!nr || !nc)
91
+ return [[], []];
92
+ const transpose = nc < nr;
93
+ let c;
94
+ if (transpose) {
95
+ c = new Array(nr * nc);
96
+ for (let i = 0; i < nr; i++)
97
+ for (let j = 0; j < nc; j++)
98
+ c[j * nr + i] = cost[i][j];
99
+ [nr, nc] = [nc, nr];
100
+ }
101
+ else
102
+ c = cost.flat();
103
+ if (c.some((x) => Number.isNaN(x) || x === -Infinity))
104
+ throw new Error('指派矩阵含 NaN 或 -inf');
105
+ const u = new Array(nr).fill(0);
106
+ const v = new Array(nc).fill(0);
107
+ const shortest = new Array(nc);
108
+ const path = new Array(nc).fill(-1);
109
+ const col4row = new Array(nr).fill(-1);
110
+ const row4col = new Array(nc).fill(-1);
111
+ const SR = new Array(nr);
112
+ const SC = new Array(nc);
113
+ const remaining = new Array(nc);
114
+ for (let cur = 0; cur < nr; cur++) {
115
+ let minVal = 0;
116
+ let i = cur;
117
+ let num = nc;
118
+ for (let it = 0; it < nc; it++)
119
+ remaining[it] = nc - it - 1;
120
+ SR.fill(false);
121
+ SC.fill(false);
122
+ shortest.fill(Infinity);
123
+ let sink = -1;
124
+ while (sink === -1) {
125
+ let index = -1;
126
+ let lowest = Infinity;
127
+ SR[i] = true;
128
+ for (let it = 0; it < num; it++) {
129
+ const j = remaining[it];
130
+ const r = minVal + c[i * nc + j] - u[i] - v[j];
131
+ if (r < shortest[j]) {
132
+ path[j] = i;
133
+ shortest[j] = r;
134
+ }
135
+ if (shortest[j] < lowest || (shortest[j] === lowest && row4col[j] === -1)) {
136
+ lowest = shortest[j];
137
+ index = it;
138
+ }
139
+ }
140
+ minVal = lowest;
141
+ if (minVal === Infinity)
142
+ throw new Error('指派问题无可行解');
143
+ const j = remaining[index];
144
+ if (row4col[j] === -1)
145
+ sink = j;
146
+ else
147
+ i = row4col[j];
148
+ SC[j] = true;
149
+ remaining[index] = remaining[--num];
150
+ }
151
+ u[cur] += minVal;
152
+ for (let r = 0; r < nr; r++)
153
+ if (SR[r] && r !== cur)
154
+ u[r] += minVal - shortest[col4row[r]];
155
+ for (let j = 0; j < nc; j++)
156
+ if (SC[j])
157
+ v[j] -= minVal - shortest[j];
158
+ let j = sink;
159
+ for (;;) {
160
+ const r = path[j];
161
+ row4col[j] = r;
162
+ [col4row[r], j] = [j, col4row[r]];
163
+ if (r === cur)
164
+ break;
165
+ }
166
+ }
167
+ if (transpose) {
168
+ const order = col4row.map((col, row) => [col, row]).sort((a, b) => a[0] - b[0] || a[1] - b[1]);
169
+ return [order.map(([col]) => col), order.map(([, row]) => row)];
170
+ }
171
+ return [col4row.map((_, i) => i), col4row];
172
+ }
173
+ const pyList = (xs) => `[${xs.map((x) => (typeof x === 'string' ? `'${x}'` : String(x))).join(', ')}]`;
174
+ const round4 = (x) => Math.round(x * 1e4) / 1e4;
175
+ /** 点选识别:geetest_metric.solve,再加上 geetest_hybrid.solve 的旁证告警。 */
176
+ export async function solveClick(spriteBytes) {
177
+ const sprite = await decodeImage(spriteBytes);
178
+ const { puzzle, hint } = splitSprite(sprite);
179
+ const rawPuzzleBoxes = await detectBoxes(puzzle);
180
+ const puzzleBoxes = mergeDuplicateBoxes(rawPuzzleBoxes);
181
+ // 提示条相邻字会轻微相交,只合并高度嵌套的重复框,再切开相邻边界
182
+ const hintBoxes = splitOverlaps(mergeDuplicateBoxes(await detectBoxes(hint), 0.75));
183
+ const hints = [];
184
+ for (const box of hintBoxes)
185
+ hints.push(await signature(cropPad(hint, box, 2)));
186
+ const cands = [];
187
+ for (const box of puzzleBoxes)
188
+ cands.push(await signature(cropPad(puzzle, box, PAD)));
189
+ if (!hints.length || cands.length < hints.length) {
190
+ throw new SpecificAIError('RISK_CONTROL', `点选识别的检测框不足:提示 ${hints.length},候选 ${cands.length}`, { detail: { kind: 'captcha' } });
191
+ }
192
+ const score = hints.map((h) => cands.map((p) => {
193
+ let dot = 0;
194
+ for (let i = 0; i < h.vector.length; i++)
195
+ dot += h.vector[i] * p.vector[i];
196
+ return Math.fround(dot);
197
+ }));
198
+ const [rows, cols] = linearSumAssignment(score.map((r) => r.map((x) => -x)));
199
+ const pairs = rows.map((r, i) => [r, cols[i]]).sort((a, b) => a[0] - b[0] || a[1] - b[1]);
200
+ const order = [];
201
+ const match = [];
202
+ for (const [h, p] of pairs) {
203
+ const box = puzzleBoxes[p];
204
+ order.push([Math.floor((box[0] + box[2]) / 2), Math.floor((box[1] + box[3]) / 2)]);
205
+ match.push({ hint: h, puzzle: p, score: round4(score[h][p]) });
206
+ }
207
+ const candChars = cands.map((c) => c.text);
208
+ const orderedChars = match.map((m) => candChars[m.puzzle]);
209
+ const warnings = [];
210
+ const low = match.filter((m) => m.score < 0.2);
211
+ if (low.length)
212
+ warnings.push(`${low.length} 个模型匹配低于 0.20,建议刷新题面`);
213
+ const blank = candChars.flatMap((c, i) => (c ? [] : [i + 1]));
214
+ if (blank.length)
215
+ warnings.push(`候选 ${pyList(blank)} ddddocr 认不出,字形匹配无旁证`);
216
+ const dupes = [...new Set(orderedChars.filter((c) => c && orderedChars.indexOf(c) !== orderedChars.lastIndexOf(c)))].sort();
217
+ if (dupes.length)
218
+ warnings.push(`点击序列里出现重复字 ${pyList(dupes)},可能有两个提示位指到了同一类字形`);
219
+ return {
220
+ order,
221
+ match,
222
+ scoreMatrix: score.map((r) => r.map(round4)),
223
+ hintText: hints.map((h) => h.text),
224
+ candChars,
225
+ orderedChars,
226
+ warnings: [...new Set(warnings)],
227
+ hintBoxes,
228
+ puzzleBoxes,
229
+ rawPuzzleBoxes,
230
+ puzzleSize: [puzzle.width, puzzle.height],
231
+ };
232
+ }
@@ -0,0 +1,450 @@
1
+ import { createCipheriv } from 'node:crypto';
2
+ import { SpecificAIError } from '../../../core/errors.js';
3
+ import { md5Hex } from '../../../core/hash.js';
4
+ import { HttpClient, parseJsonp } from '../../../core/http.js';
5
+ import { interactive } from '../../../core/login.js';
6
+ import { jsonDumps } from '../../../core/py.js';
7
+ import * as rand from '../../../core/rand.js';
8
+ import * as api from './api.js';
9
+ import { fillSessionCookies, retrying } from './client.js';
10
+ import { manualGeetest } from './geetest-manual.js';
11
+ import { BROWSER, COOKIE_ORDER, PASSPORT, PROFILE } from './profile.js';
12
+ /**
13
+ * 极验 v3(上游 utils/geetest_w.py、tools/geetest_solve.py):
14
+ * `w` 的加密原语、fullpage 无感通道,以及降级到点选题后的自动识别(geetest-vision.ts)→ 提交 → 失败换题重试。
15
+ * 自动识别连续失败时,终端里改为在本地页面上手动验证(geetest-manual.ts)。
16
+ */
17
+ const AES_IV = Buffer.from('0000000000000000');
18
+ const RSA_N_HEX = '00C1E3934D1614465B33053E7F48EE4EC87B14B95EF88947713D25EECBFF7E74' +
19
+ 'C7977D02DC1D9451F79DD5D1C10C29ACB6A9B4D6FB7D0A0279B6719E1772565F' +
20
+ '09AF627715919221AEF91899CAE08C0D686D748B20A3603BE2318CA6BC2B5970' +
21
+ '6592A9219D0BF05C9F65023A21D2330807252AE0066D59CEEFA5F2748EA80BAB81';
22
+ const B64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789()';
23
+ const B64_MASKS = [0x6f0000, 0x90b400, 0x004b14, 0x0000eb];
24
+ export const GEETEST_VERSION = '9.2.0-guwyxh';
25
+ export function genAesKey(length = 16) {
26
+ return rand.string(length, '0123456789abcdef');
27
+ }
28
+ export function aesEncrypt(plaintext, key) {
29
+ const c = createCipheriv('aes-128-cbc', Buffer.from(key), AES_IV);
30
+ return Buffer.concat([c.update(plaintext, 'utf8'), c.final()]);
31
+ }
32
+ function gatherBits(value, mask) {
33
+ let n = 0;
34
+ for (let r = 23; r >= 0; r--)
35
+ if ((mask >> r) & 1)
36
+ n = (n << 1) + ((value >> r) & 1);
37
+ return n;
38
+ }
39
+ /** 极验自定义 base64:按掩码做位挑选,尾部用 '.' 填充。 */
40
+ export function customB64(data) {
41
+ let res = '';
42
+ let end = '';
43
+ const size = data.length;
44
+ for (let a = 0; a < size; a += 3) {
45
+ let v;
46
+ let take;
47
+ if (a + 2 < size) {
48
+ v = (data[a] << 16) + (data[a + 1] << 8) + data[a + 2];
49
+ take = 4;
50
+ }
51
+ else if (size % 3 === 2) {
52
+ v = (data[a] << 16) + (data[a + 1] << 8);
53
+ take = 3;
54
+ end = '.';
55
+ }
56
+ else {
57
+ v = data[a] << 16;
58
+ take = 2;
59
+ end = '..';
60
+ }
61
+ for (const m of B64_MASKS.slice(0, take))
62
+ res += B64_ALPHABET[gatherBits(v, m)];
63
+ }
64
+ return res + end;
65
+ }
66
+ const RSA_N = BigInt('0x' + RSA_N_HEX);
67
+ const RSA_E = 0x10001n;
68
+ const RSA_BYTES = 128;
69
+ function modPow(base, exp, mod) {
70
+ let result = 1n;
71
+ base %= mod;
72
+ for (; exp > 0n; exp >>= 1n) {
73
+ if (exp & 1n)
74
+ result = (result * base) % mod;
75
+ base = (base * base) % mod;
76
+ }
77
+ return result;
78
+ }
79
+ /**
80
+ * 用极验公钥加密 AES 密钥(PKCS#1 v1.5),十六进制。填充的非零随机字节逐个从 rand.bytes 取(取到 0 就重取),
81
+ * 这样对拍时与上游(gen.py 换成确定性随机字节的同一套填充)逐字节一致;平时就是 crypto.randomBytes。
82
+ */
83
+ export function rsaEncryptKey(key) {
84
+ const m = Buffer.from(key);
85
+ const ps = [];
86
+ while (ps.length < RSA_BYTES - m.length - 3) {
87
+ const b = rand.bytes(1)[0];
88
+ if (b)
89
+ ps.push(b);
90
+ }
91
+ const em = Buffer.concat([Buffer.from([0, 2]), Buffer.from(ps), Buffer.from([0]), m]);
92
+ return modPow(BigInt('0x' + em.toString('hex')), RSA_E, RSA_N).toString(16).padStart(RSA_BYTES * 2, '0');
93
+ }
94
+ /** `tt` 字段的插入式混淆;取模用原始 track 的长度。 */
95
+ export function csCipher(track, c, s) {
96
+ if (!c?.length || !s)
97
+ return track;
98
+ const [s0, , a, , tail] = c;
99
+ let result = track;
100
+ const base = track.length;
101
+ for (let offset = 0; offset < s.length - 1; offset += 2) {
102
+ const ch = parseInt(s.slice(offset, offset + 2), 16);
103
+ const pos = (s0 * ch * ch + a * ch + tail) % base;
104
+ result = result.slice(0, pos) + String.fromCharCode(ch) + result.slice(pos);
105
+ }
106
+ return result;
107
+ }
108
+ const EMPTY_TRACK = 'M(*((1((M((';
109
+ const EMPTY_HDL_TRACK = 'tEQOYESJYERVYEQ.';
110
+ const EMPTY_BUF_MAGIC = '-1magic data'.repeat(73) + '-1';
111
+ const EMPTY_BUF_BANG = '-1!!'.repeat(73) + '-1';
112
+ const EMPTY_HDL_N = 'dGFdxFsdzEBYxHgZ'.repeat(73) + 'dGE.';
113
+ export function defaultEp(nowMs = rand.now()) {
114
+ const tm = {};
115
+ [...'abcdefghijklmnopqrstu'].forEach((k, i) => (tm[k] = 'afghijlmnopqr'.includes(k) ? nowMs + i : 0));
116
+ return {
117
+ v: GEETEST_VERSION,
118
+ te: false,
119
+ $_BBn: false,
120
+ ven: PROFILE.webglVendor,
121
+ ren: PROFILE.webglRenderer,
122
+ fp: null,
123
+ lp: null,
124
+ em: { ph: 0, cp: 0, ek: '11', wd: 1, nt: 0, si: 0, sc: 0 },
125
+ tm,
126
+ dnf: 'dnf',
127
+ by: 2,
128
+ };
129
+ }
130
+ /** 极验手工拼的 JSON:`"key":value`,值用 Python json.dumps 的默认分隔符。 */
131
+ function stringify(fields) {
132
+ return '{' + fields.map(([k, v]) => `${jsonDumps(k)}:${jsonDumps(v, { ensureAscii: false })}`).join(',') + '}';
133
+ }
134
+ const tt = (c, s) => (c?.length && s ? csCipher(EMPTY_TRACK, c, s) : EMPTY_TRACK);
135
+ /** fullpage(无感判定)的明文载荷(build_payload)。 */
136
+ export function buildPayload(gt, challenge, passtime, c, s = '', ep = defaultEp()) {
137
+ return stringify([
138
+ ['lang', 'zh-cn'],
139
+ ['type', 'fullpage'],
140
+ ['tt', tt(c, s)],
141
+ ['light', -1],
142
+ ['s', md5Hex(EMPTY_HDL_TRACK)],
143
+ ['h', md5Hex(EMPTY_HDL_N)],
144
+ ['hh', md5Hex(EMPTY_BUF_MAGIC)],
145
+ ['hi', md5Hex(EMPTY_BUF_BANG)],
146
+ ['vip_order', -1],
147
+ ['ct', -1],
148
+ ['ep', ep],
149
+ ['passtime', passtime],
150
+ ['rp', md5Hex(`${gt}${challenge}${passtime}`)],
151
+ ]);
152
+ }
153
+ /** 第一发 get.php 的明文载荷(build_init_payload)。 */
154
+ export function buildInitPayload(gt, challenge) {
155
+ return jsonDumps({
156
+ gt,
157
+ challenge,
158
+ offline: false,
159
+ new_captcha: true,
160
+ product: 'bind',
161
+ https: true,
162
+ lang: 'zh-cn',
163
+ type: 'fullpage',
164
+ protocol: 'https://',
165
+ width: '300px',
166
+ cc: 20,
167
+ ww: true,
168
+ i: EMPTY_BUF_BANG,
169
+ }, { separators: [',', ':'], ensureAscii: false });
170
+ }
171
+ /** 点选提交(ajax.php)的明文载荷(build_click_payload):有 a 时才带 rp。 */
172
+ export function buildClickPayload(gt, challenge, a, pic, passtime, c, s = '', ep = defaultEp()) {
173
+ const fields = [
174
+ ['lang', 'zh-cn'],
175
+ ['passtime', passtime],
176
+ ['a', a],
177
+ ['pic', pic],
178
+ ['tt', tt(c, s)],
179
+ ['ep', ep],
180
+ ];
181
+ if (a)
182
+ fields.push(['rp', md5Hex(`${gt}${challenge}${passtime}`)]);
183
+ return stringify(fields);
184
+ }
185
+ /** JS 的 Math.round(.5 一律向 +∞),即上游的 _js_round。 */
186
+ const jsRound = (x) => Math.floor(x + 0.5);
187
+ /** 点击坐标 → `a` 字段:`x_y,x_y`,坐标是相对图片宽高的万分比(encode_click_a)。 */
188
+ export function encodeClickA(clicks) {
189
+ return clicks.map(([x, y]) => `${Math.trunc(x)}_${Math.trunc(y)}`).join(',');
190
+ }
191
+ /** 0~1 的相对坐标 → `a` 字段(encode_click_a_from_ratio)。 */
192
+ export function encodeClickAFromRatio(ratios) {
193
+ return encodeClickA(ratios.map(([rx, ry]) => [jsRound(rx * 10000), jsRound(ry * 10000)]));
194
+ }
195
+ export function buildW(payload, key = genAesKey(), withRsa = true) {
196
+ const body = customB64(aesEncrypt(payload, key));
197
+ return withRsa ? body + rsaEncryptKey(key) : body;
198
+ }
199
+ // ---------------------------------------------------------------- 链路
200
+ const GEETEST = 'https://api.geetest.com';
201
+ const REFERER = 'https://passport.bilibili.com/';
202
+ /** 点选最多试几道题(上游 BiliAuth.from_sms_login 的 attempts=4)。 */
203
+ const ATTEMPTS = 4;
204
+ /** trans_cookies:Cookie 串 → 有序的名值表。 */
205
+ function parseCookies(str) {
206
+ const out = new Map();
207
+ for (let item of str.split(';')) {
208
+ item = item.trim();
209
+ const i = item.indexOf('=');
210
+ if (!item || i < 0)
211
+ continue;
212
+ out.set(item.slice(0, i).trim(), item.slice(i + 1).trim());
213
+ }
214
+ return out;
215
+ }
216
+ const cookieString = (c) => [...c].map(([k, v]) => `${k}=${v}`).join('; ');
217
+ /** sort_cookies:按浏览器的 cookie 顺序重排,表外的按原顺序排在最后。 */
218
+ function sortCookies(c) {
219
+ const known = COOKIE_ORDER.filter((k) => c.has(k));
220
+ return new Map([...known, ...[...c.keys()].filter((k) => !COOKIE_ORDER.includes(k))].map((k) => [k, c.get(k)]));
221
+ }
222
+ /**
223
+ * 上游 `BiliAuth.from_cookie(state['cookies'], fill_device=False)` 的 cookie:极验 cookie 之外补上会话级的
224
+ * sid / PVID / b_lsid,再按浏览器顺序排好。上游提交和换题都用这份 cookie 请求 api.geetest.com。
225
+ */
226
+ function sessionCookies(str) {
227
+ const c = parseCookies(str);
228
+ fillSessionCookies((name) => c.has(name), (name, value) => c.set(name, value));
229
+ return sortCookies(c);
230
+ }
231
+ /** 响应里新设的 cookie(requests_cookies_to_dict)。 */
232
+ function setCookies(res) {
233
+ return res.headers.getSetCookie().map((line) => {
234
+ const i = line.indexOf('=');
235
+ return [line.slice(0, i).trim(), line.slice(i + 1).split(';', 1)[0].trim()];
236
+ });
237
+ }
238
+ /**
239
+ * 极验的请求:只带 referer(上游 `request(..., headers={'referer': REFERER})`),cookie 每次显式给出。
240
+ * 用单独的 HTTP 客户端,极验的 cookie 不进 B 站凭证的 cookie 罐。
241
+ */
242
+ class Geetest {
243
+ b;
244
+ http;
245
+ constructor(b) {
246
+ this.b = b;
247
+ const { ctx } = b;
248
+ this.http = new HttpClient({ browser: BROWSER, os: 'windows', proxy: ctx.config.proxy, timeout: ctx.config.timeout, log: ctx.log });
249
+ }
250
+ /** 带退避重试的 GET(上游 http_util.request)。 */
251
+ get(url, query, cookies) {
252
+ return retrying(() => this.http.request({ url, query, headers: [['referer', REFERER]], cookies: Object.fromEntries(cookies) }), this.b.ctx.signal);
253
+ }
254
+ /** JSONP 接口:自动带 callback,返回解析后的对象和新设的 cookie。 */
255
+ async call(path, query, cookies) {
256
+ const res = await this.get(`${GEETEST}${path}`, [...query, ['callback', `geetest_${rand.now()}`]], cookies);
257
+ const text = await res.text();
258
+ const body = parseJsonp(text);
259
+ if (body == null)
260
+ throw new SpecificAIError('UPSTREAM', `极验 ${path} 返回的不是 JSON(HTTP ${res.status})`, { detail: { status: res.status, body: text.slice(0, 300) } });
261
+ return { body, cookies: setCookies(res) };
262
+ }
263
+ /** 下载题图。 */
264
+ async image(servers, pic, cookies) {
265
+ const server = (servers?.[0] ?? 'static.geetest.com/').replace(/\/+$/, '');
266
+ const res = await this.get(`https://${server}/${pic.replace(/^\/+/, '')}`, undefined, cookies);
267
+ if (res.status !== 200)
268
+ throw captchaError(`下载极验题图失败(HTTP ${res.status})`);
269
+ return new Uint8Array(await res.arrayBuffer());
270
+ }
271
+ }
272
+ function captchaError(message, detail = {}) {
273
+ return new SpecificAIError('RISK_CONTROL', message, { detail: { kind: 'captcha', ...detail } });
274
+ }
275
+ /** B 站会话的 cookie(上游 `auth.cookie`)。上游下载第一张题图时没传 cookies,带的就是这一份。 */
276
+ function biliCookies(b) {
277
+ return new Map(b.http.prepare({ url: `${PASSPORT}/` }).cookies);
278
+ }
279
+ /** 向 B 站申请一次人机验证(上游 get_captcha),返回 token 与极验的 gt / challenge。 */
280
+ async function requestCaptcha(b) {
281
+ const captcha = await api.captcha(b);
282
+ if (captcha.code !== 0)
283
+ throw new SpecificAIError('UPSTREAM', `申请人机验证失败:${captcha.message ?? captcha.code}`, { detail: { code: captcha.code } });
284
+ return { token: captcha.data.token, gt: captcha.data.geetest.gt, challenge: captcha.data.geetest.challenge };
285
+ }
286
+ /**
287
+ * 申请 B 站 captcha,走 fullpage 无感判定(上游 fetch):
288
+ * 1. get.php(w = AES + RSA)建立会话,拿 c / s;
289
+ * 2. ajax.php(w 复用 AES)无感判定,直接放行时返回 validate;
290
+ * 3. 降级到点选时,get.php?is_next=true&type=click 下发题图和新的 c / s,再下载题图。
291
+ */
292
+ async function fetchPuzzle(g) {
293
+ const { b } = g;
294
+ const { token, gt, challenge } = await requestCaptcha(b);
295
+ const type = (await g.call('/gettype.php', [['gt', gt]], new Map())).body?.data?.type;
296
+ b.ctx.log.debug(`极验产品类型:${type}`);
297
+ const cookies = new Map();
298
+ const key = genAesKey();
299
+ const common = [
300
+ ['gt', gt],
301
+ ['challenge', challenge],
302
+ ['lang', 'zh-cn'],
303
+ ['pt', 0],
304
+ ['client_type', 'web'],
305
+ ];
306
+ const init = await g.call('/get.php', [...common, ['w', buildW(buildInitPayload(gt, challenge), key, true)]], cookies);
307
+ for (const [k, v] of init.cookies)
308
+ cookies.set(k, v);
309
+ if (init.body.status !== 'success')
310
+ throw captchaError('人机验证初始化被拒绝', { response: init.body });
311
+ const { c, s } = init.body.data ?? {};
312
+ const startedAt = rand.now();
313
+ const decision = await g.call('/ajax.php', [...common, ['w', buildW(buildPayload(gt, challenge, 800, c, s ?? ''), key, false)]], cookies);
314
+ for (const [k, v] of decision.cookies)
315
+ cookies.set(k, v);
316
+ const result = decision.body.data?.result;
317
+ b.ctx.log.debug(`极验 fullpage 判定:${result}`);
318
+ if (result !== 'click') {
319
+ const validate = decision.body.data?.validate;
320
+ if (validate)
321
+ return { validate: { token, challenge, validate, seccode: `${validate}|jordan` } };
322
+ throw captchaError(`极验没有放行,也没有下发点选题:${result ?? decision.body.status}`, { result: result ?? null });
323
+ }
324
+ const puzzle = await g.call('/get.php', [
325
+ ['is_next', 'true'],
326
+ ['type', 'click'],
327
+ ['gt', gt],
328
+ ['challenge', challenge],
329
+ ['lang', 'zh-cn'],
330
+ ['https', 'true'],
331
+ ['protocol', 'https://'],
332
+ ['offline', 'false'],
333
+ ['product', 'embed'],
334
+ ['api_server', 'api.geetest.com'],
335
+ ['isPC', 'true'],
336
+ ['autoReset', 'true'],
337
+ ['width', '100%'],
338
+ ], cookies);
339
+ for (const [k, v] of puzzle.cookies)
340
+ cookies.set(k, v);
341
+ if (puzzle.body.status !== 'success')
342
+ throw captchaError('极验拒绝下发点选题', { response: puzzle.body });
343
+ const data = puzzle.body.data ?? {};
344
+ if (!data.pic)
345
+ throw captchaError('极验没有下发点选题图', { type: data.pic_type ?? null });
346
+ const sprite = await g.image(data.static_servers, data.pic, biliCookies(b));
347
+ return {
348
+ state: { gt, challenge, token, pic: data.pic, c: data.c ?? null, s: data.s ?? '', cookies: cookieString(cookies), fetchedAt: startedAt },
349
+ sprite,
350
+ };
351
+ }
352
+ /** 提交点击坐标(上游 _post_ajax)。click 插件会重新生成 AES 密钥,并且总带 RSA 段。 */
353
+ async function postAjax(g, state, a) {
354
+ // 服务端会拿请求到达时间校验 passtime:识别太快时补足等待,避免 `duration short`
355
+ const elapsed = rand.now() - state.fetchedAt;
356
+ if (elapsed < 3500)
357
+ await rand.sleep(3500 - elapsed, g.b.ctx.signal);
358
+ const passtime = rand.now() - state.fetchedAt;
359
+ const w = buildW(buildClickPayload(state.gt, state.challenge, a, state.pic, passtime, state.c, state.s), undefined, true);
360
+ const { body } = await g.call('/ajax.php', [
361
+ ['gt', state.gt],
362
+ ['challenge', state.challenge],
363
+ ['lang', 'zh-cn'],
364
+ ['pt', 0],
365
+ ['client_type', 'web'],
366
+ ['w', w],
367
+ ], sessionCookies(state.cookies));
368
+ return body;
369
+ }
370
+ /** 验证失败后换一道题(上游 _refresh,对齐 click.3.1.2.js 的 refresh.php)。c / s 沿用原来的。 */
371
+ async function refresh(g, state) {
372
+ const cookies = sessionCookies(state.cookies);
373
+ const { body, cookies: set } = await g.call('/refresh.php', [
374
+ ['gt', state.gt],
375
+ ['challenge', state.challenge],
376
+ ['lang', 'zh-cn'],
377
+ ['type', 'click'],
378
+ ], cookies);
379
+ for (const [k, v] of set)
380
+ cookies.set(k, v);
381
+ const sorted = sortCookies(cookies);
382
+ const data = body.data ?? {};
383
+ if (body.status !== 'success' || !data.pic)
384
+ throw captchaError('极验拒绝换题', { response: body });
385
+ const sprite = await g.image(data.image_servers, data.pic, sorted);
386
+ return { state: { ...state, pic: data.pic, cookies: cookieString(sorted), fetchedAt: rand.now() }, sprite };
387
+ }
388
+ /**
389
+ * 自动过极验(上游 tools/geetest_solve.solve):无感通道放行就直接返回;降级到点选时自动识别并提交,
390
+ * 服务端判 fail 就换题重试,最多 attempts 道题。识别或提交失败报 RISK_CONTROL(captcha)。
391
+ */
392
+ export async function autoSolve(b, attempts = ATTEMPTS) {
393
+ const g = new Geetest(b);
394
+ const first = await fetchPuzzle(g);
395
+ if ('validate' in first)
396
+ return first.validate;
397
+ let { state, sprite } = first;
398
+ const { solveClick } = await import('./geetest-vision.js');
399
+ let body;
400
+ for (let attempt = 1; attempt <= attempts; attempt++) {
401
+ const r = await solveClick(sprite).catch((err) => {
402
+ if (err instanceof SpecificAIError)
403
+ throw err;
404
+ throw captchaError(`点选识别出错:${err instanceof Error ? err.message : String(err)}`);
405
+ });
406
+ b.ctx.log.debug(`极验点选第 ${attempt}/${attempts} 题:提示 ${r.hintText.join(' ')},候选 ${r.candChars.join(' ')},点击 ${JSON.stringify(r.order)}`, r.match);
407
+ for (const w of r.warnings)
408
+ b.ctx.log.debug(`极验点选:${w}`);
409
+ const [width, height] = r.puzzleSize;
410
+ const a = encodeClickAFromRatio(r.order.map(([x, y]) => [x / width, y / height]));
411
+ body = await postAjax(g, state, a);
412
+ const validate = body?.data?.validate;
413
+ if (validate)
414
+ return { token: state.token, challenge: state.challenge, validate, seccode: `${validate}|jordan` };
415
+ if (body?.data?.result !== 'fail' || attempt === attempts)
416
+ break;
417
+ ({ state, sprite } = await refresh(g, state));
418
+ }
419
+ throw captchaError(`极验点选在 ${attempts} 道题内没有通过:${body?.data?.result ?? body?.status ?? '未知结果'}`, { result: body?.data?.result ?? null });
420
+ }
421
+ /** 自动识别失败后是否该交给人:验证码本身的失败(RISK_CONTROL),或者识别环境有问题(ERROR,例如缺模型)。 */
422
+ function isSolveFailure(err) {
423
+ return err instanceof SpecificAIError && (err.code === 'RISK_CONTROL' || err.code === 'ERROR');
424
+ }
425
+ /**
426
+ * 登录用的极验:先自动过(无感 / 点选识别);自动识别失败时,终端里(stdin 与 stderr 都是 TTY)重新申请一次 captcha,
427
+ * 在本地页面上手动验证(上游 quick_sms_login.complete_geetest 的人工分支);否则报 RISK_CONTROL。
428
+ */
429
+ export async function solve(b) {
430
+ try {
431
+ return await autoSolve(b);
432
+ }
433
+ catch (err) {
434
+ if (!isSolveFailure(err))
435
+ throw err;
436
+ if (!interactive()) {
437
+ // 环境问题(例如缺 OCR 模型)原样报出,带着它自己的修复提示
438
+ if (err.code === 'ERROR')
439
+ throw err;
440
+ throw new SpecificAIError('RISK_CONTROL', `人机验证没有自动通过:${err.message}`, {
441
+ hint: '在终端里重新执行这条登录命令:自动识别失败时会在本地打开验证页面,在浏览器里手动完成即可;也可以改用扫码登录 specific-ai bilibili auth login --method qrcode',
442
+ detail: { kind: 'captcha', ...(err.detail && typeof err.detail === 'object' ? err.detail : {}) },
443
+ });
444
+ }
445
+ b.ctx.log.warn(`人机验证没有自动通过(${err.message}),改为在浏览器里手动验证`);
446
+ const { token, gt, challenge } = await requestCaptcha(b);
447
+ const r = await manualGeetest(b.ctx, gt, challenge);
448
+ return { token, challenge: r.challenge, validate: r.validate, seccode: r.seccode };
449
+ }
450
+ }