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.
- package/LICENSE +21 -0
- package/README.en.md +177 -0
- package/README.ja.md +177 -0
- package/README.ko.md +177 -0
- package/README.md +174 -2
- package/README.zh-TW.md +177 -0
- package/assets/banner.txt +3 -0
- package/dist/cli/argv.js +98 -0
- package/dist/cli/commands/auth.js +11 -0
- package/dist/cli/commands/config.js +23 -0
- package/dist/cli/commands/doctor.js +134 -0
- package/dist/cli/commands/platforms.js +24 -0
- package/dist/cli/commands/version.js +4 -0
- package/dist/cli/confirm.js +22 -0
- package/dist/cli/dispatch.js +440 -0
- package/dist/cli/help.js +223 -0
- package/dist/cli/hints.js +60 -0
- package/dist/cli/main.js +18 -0
- package/dist/cli/output.js +66 -0
- package/dist/core/auth-handlers.js +41 -0
- package/dist/core/auth-store.js +119 -0
- package/dist/core/capabilities.js +163 -0
- package/dist/core/config.js +121 -0
- package/dist/core/cookies.js +164 -0
- package/dist/core/errors.js +37 -0
- package/dist/core/files.js +89 -0
- package/dist/core/fsutil.js +52 -0
- package/dist/core/hash.js +4 -0
- package/dist/core/http.js +331 -0
- package/dist/core/image.js +280 -0
- package/dist/core/log.js +40 -0
- package/dist/core/login.js +191 -0
- package/dist/core/mp4.js +130 -0
- package/dist/core/normalize.js +207 -0
- package/dist/core/options.js +110 -0
- package/dist/core/paths.js +38 -0
- package/dist/core/pb.js +59 -0
- package/dist/core/py.js +181 -0
- package/dist/core/rand.js +131 -0
- package/dist/core/registry.js +129 -0
- package/dist/core/schemas.js +41 -0
- package/dist/core/stream.js +104 -0
- package/dist/core/toolkit.js +35 -0
- package/dist/core/vm.js +73 -0
- package/dist/core/vocab.js +157 -0
- package/dist/platforms/_shared/impaas.js +410 -0
- package/dist/platforms/bilibili/index.js +216 -0
- package/dist/platforms/bilibili/web/api.js +806 -0
- package/dist/platforms/bilibili/web/client.js +311 -0
- package/dist/platforms/bilibili/web/commands.js +783 -0
- package/dist/platforms/bilibili/web/ddddocr.js +342 -0
- package/dist/platforms/bilibili/web/gaia.js +130 -0
- package/dist/platforms/bilibili/web/geetest-manual.js +154 -0
- package/dist/platforms/bilibili/web/geetest-vision.js +232 -0
- package/dist/platforms/bilibili/web/geetest.js +450 -0
- package/dist/platforms/bilibili/web/normalize.js +234 -0
- package/dist/platforms/bilibili/web/profile.js +135 -0
- package/dist/platforms/bilibili/web/resolve.js +112 -0
- package/dist/platforms/bilibili/web/sign.js +223 -0
- package/dist/platforms/bilibili/web/upos.js +102 -0
- package/dist/platforms/douyin/index.js +142 -0
- package/dist/platforms/douyin/web/api.js +863 -0
- package/dist/platforms/douyin/web/client.js +413 -0
- package/dist/platforms/douyin/web/commands.js +712 -0
- package/dist/platforms/douyin/web/creator.js +601 -0
- package/dist/platforms/douyin/web/crypto.js +247 -0
- package/dist/platforms/douyin/web/dtrait.js +190 -0
- package/dist/platforms/douyin/web/im.js +502 -0
- package/dist/platforms/douyin/web/image.js +5 -0
- package/dist/platforms/douyin/web/live.js +225 -0
- package/dist/platforms/douyin/web/mssdk.js +148 -0
- package/dist/platforms/douyin/web/normalize.js +326 -0
- package/dist/platforms/douyin/web/passport.js +1247 -0
- package/dist/platforms/douyin/web/profile.js +221 -0
- package/dist/platforms/douyin/web/proto.js +26 -0
- package/dist/platforms/douyin/web/resolve.js +88 -0
- package/dist/platforms/douyin/web/sign.js +436 -0
- package/dist/platforms/douyin/web/tos.js +78 -0
- package/dist/platforms/index.js +17 -0
- package/dist/platforms/jd/index.js +67 -0
- package/dist/platforms/jd/web/aks.js +115 -0
- package/dist/platforms/jd/web/api.js +549 -0
- package/dist/platforms/jd/web/chat.js +132 -0
- package/dist/platforms/jd/web/client.js +470 -0
- package/dist/platforms/jd/web/commands.js +557 -0
- package/dist/platforms/jd/web/jcap/helper.js +90 -0
- package/dist/platforms/jd/web/jcap/host.js +107 -0
- package/dist/platforms/jd/web/jcap/lsd.js +300 -0
- package/dist/platforms/jd/web/jcap/solver.js +995 -0
- package/dist/platforms/jd/web/jcap.js +150 -0
- package/dist/platforms/jd/web/js.js +420 -0
- package/dist/platforms/jd/web/login.js +775 -0
- package/dist/platforms/jd/web/normalize.js +297 -0
- package/dist/platforms/jd/web/profile.js +256 -0
- package/dist/platforms/jd/web/util.js +175 -0
- package/dist/platforms/jd/web/webm.js +133 -0
- package/dist/platforms/kuaishou/index.js +98 -0
- package/dist/platforms/kuaishou/web/api.js +694 -0
- package/dist/platforms/kuaishou/web/captcha-crypto.js +134 -0
- package/dist/platforms/kuaishou/web/captcha-fp.js +180 -0
- package/dist/platforms/kuaishou/web/captcha.js +273 -0
- package/dist/platforms/kuaishou/web/client.js +487 -0
- package/dist/platforms/kuaishou/web/commands.js +863 -0
- package/dist/platforms/kuaishou/web/gap.js +144 -0
- package/dist/platforms/kuaishou/web/gdfp.js +376 -0
- package/dist/platforms/kuaishou/web/gql.js +11 -0
- package/dist/platforms/kuaishou/web/live.js +125 -0
- package/dist/platforms/kuaishou/web/normalize.js +241 -0
- package/dist/platforms/kuaishou/web/oracle.js +329 -0
- package/dist/platforms/kuaishou/web/profile.js +259 -0
- package/dist/platforms/kuaishou/web/resolve.js +84 -0
- package/dist/platforms/kuaishou/web/session.js +592 -0
- package/dist/platforms/kuaishou/web/sign.js +568 -0
- package/dist/platforms/taobao/index.js +56 -0
- package/dist/platforms/taobao/web/api.js +96 -0
- package/dist/platforms/taobao/web/client.js +102 -0
- package/dist/platforms/taobao/web/commands.js +172 -0
- package/dist/platforms/taobao/web/im.js +68 -0
- package/dist/platforms/taobao/web/normalize.js +103 -0
- package/dist/platforms/taobao/web/profile.js +85 -0
- package/dist/platforms/taobao/web/resolve.js +51 -0
- package/dist/platforms/taobao/web/sign.js +35 -0
- package/dist/platforms/tiktok/index.js +120 -0
- package/dist/platforms/tiktok/web/api.js +1234 -0
- package/dist/platforms/tiktok/web/client.js +362 -0
- package/dist/platforms/tiktok/web/commands.js +821 -0
- package/dist/platforms/tiktok/web/im.js +133 -0
- package/dist/platforms/tiktok/web/jsrun.js +126 -0
- package/dist/platforms/tiktok/web/normalize.js +286 -0
- package/dist/platforms/tiktok/web/profile.js +169 -0
- package/dist/platforms/tiktok/web/resolve.js +156 -0
- package/dist/platforms/tiktok/web/sign.js +585 -0
- package/dist/platforms/tiktok/web/upload.js +805 -0
- package/dist/platforms/tiktok/web/wire.js +490 -0
- package/dist/platforms/weibo/index.js +91 -0
- package/dist/platforms/weibo/web/api.js +206 -0
- package/dist/platforms/weibo/web/client.js +155 -0
- package/dist/platforms/weibo/web/commands.js +202 -0
- package/dist/platforms/weibo/web/normalize.js +137 -0
- package/dist/platforms/weibo/web/profile.js +246 -0
- package/dist/platforms/weibo/web/resolve.js +60 -0
- package/dist/platforms/weibo/web/sign.js +85 -0
- package/dist/platforms/x/index.js +107 -0
- package/dist/platforms/x/web/api.js +443 -0
- package/dist/platforms/x/web/article.js +212 -0
- package/dist/platforms/x/web/client.js +202 -0
- package/dist/platforms/x/web/commands.js +363 -0
- package/dist/platforms/x/web/graphql.js +108 -0
- package/dist/platforms/x/web/normalize.js +235 -0
- package/dist/platforms/x/web/profile.js +99 -0
- package/dist/platforms/x/web/resolve.js +44 -0
- package/dist/platforms/x/web/transaction.js +171 -0
- package/dist/platforms/xhs/index.js +101 -0
- package/dist/platforms/xhs/web/api.js +278 -0
- package/dist/platforms/xhs/web/client.js +540 -0
- package/dist/platforms/xhs/web/commands.js +1046 -0
- package/dist/platforms/xhs/web/creator-api.js +609 -0
- package/dist/platforms/xhs/web/creator.js +203 -0
- package/dist/platforms/xhs/web/js.js +194 -0
- package/dist/platforms/xhs/web/login.js +230 -0
- package/dist/platforms/xhs/web/normalize.js +166 -0
- package/dist/platforms/xhs/web/profile.js +127 -0
- package/dist/platforms/xhs/web/push.js +361 -0
- package/dist/platforms/xhs/web/resolve.js +69 -0
- package/dist/platforms/xhs/web/state.js +368 -0
- package/dist/platforms/xianyu/index.js +59 -0
- package/dist/platforms/xianyu/web/api.js +312 -0
- package/dist/platforms/xianyu/web/client.js +134 -0
- package/dist/platforms/xianyu/web/commands.js +305 -0
- package/dist/platforms/xianyu/web/im.js +73 -0
- package/dist/platforms/xianyu/web/normalize.js +123 -0
- package/dist/platforms/xianyu/web/profile.js +146 -0
- package/dist/platforms/xianyu/web/resolve.js +48 -0
- package/dist/platforms/xianyu/web/sign.js +79 -0
- package/package.json +77 -4
- package/static/douyin/Live.proto +194 -0
- package/static/douyin/Request.proto +89 -0
- package/static/douyin/Response.proto +46 -0
- package/static/douyin/acrawler_runtime/ac_vm.js +2 -0
- package/static/douyin/acrawler_runtime/browser_window_shape.json +1 -0
- package/static/douyin/acrawler_runtime/canvas_actual_exact.json +1 -0
- package/static/douyin/acrawler_runtime/run_ac_node.js +1005 -0
- package/static/douyin/acrawler_runtime/runner_window_keys_baseline.txt +240 -0
- package/static/douyin/challenge_profile.json +801 -0
- package/static/douyin/challenge_template_runner.js +217 -0
- package/static/douyin/dtrait_profile.json +66 -0
- package/static/douyin/mstoken_common_profile.json +1 -0
- package/static/douyin/mstoken_profile.json +214 -0
- package/static/jd/h5st5_env.js +750 -0
- package/static/jd/h5st5_lib.js +5 -0
- package/static/jd/h5st5_server.js +49 -0
- package/static/jd/jcap/env/env_core.js +512 -0
- package/static/jd/jcap/env/run.js +1307 -0
- package/static/jd/jcap/run/jcap_ujb96b.js +39 -0
- package/static/jd/package.json +4 -0
- package/static/jd/pc_tk_lib.js +1 -0
- package/static/jd/pc_tk_server.js +63 -0
- package/static/jd/summer_cryptico_runner.js +50 -0
- package/static/jd/webm/env/run.js +453 -0
- package/static/jd/webm/run/jdwebm-riskhandle.js +49 -0
- package/static/kuaishou/bundles/weapon/kwf-0.0.2.2cee19b4b7dec496.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-0-0.0.1-obfuscated.4b7f3de1ab0ddcc2.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-1-0.0.1-obfuscated.346ebdb1c78df316.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-10-0.0.1-obfuscated.a33db41897b983af.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-11-0.0.1-obfuscated.5e0a90af726d8a7e.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-12-0.0.1-obfuscated.20ea29345706ec8c.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-13-0.0.1-obfuscated.6b74e9640ff18648.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-14-0.0.1-obfuscated.551e4429d156bf66.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-15-0.0.1-obfuscated.8625b087aaba4a60.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-16-0.0.1-obfuscated.d5ffa4020f3be99a.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-17-0.0.1-obfuscated.6d13e19b22f50a80.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-18-0.0.1-obfuscated.6d31405044166289.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-19-0.0.1-obfuscated.a9a8d925942f19e4.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-2-0.0.1-obfuscated.b849a136ae4a6b3e.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-3-0.0.1-obfuscated.f9e77ebf6186e2ac.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-4-0.0.1-obfuscated.2c3c155aa52ceb0e.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-5-0.0.1-obfuscated.4e553d17691b3c5a.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-6-0.0.1-obfuscated.f73ddd3ff13e1217.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-7-0.0.1-obfuscated.1628b8729beaab7e.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-8-0.0.1-obfuscated.6da6baef7aa81fff.js +1 -0
- package/static/kuaishou/bundles/weapon/kws-9-0.0.1-obfuscated.8645d55e5323b743.js +1 -0
- package/static/kuaishou/fixtures/current_follow_script_813.network-response +2 -0
- package/static/kuaishou/fixtures/live_ws_maps.json +61 -0
- package/static/kuaishou/fixtures/live_ws_proto.json +1342 -0
- package/static/kuaishou/js/cp-kwf.js +1 -0
- package/static/kuaishou/tools/kwf_oracle.js +799 -0
- package/static/kuaishou/tools/kws_oracle.js +885 -0
- package/static/kuaishou/tools/like_token_oracle.js +70 -0
- package/static/taobao/taobao_js_20260407.js +570 -0
- package/static/tiktok/Tiktok_Request.json +597 -0
- package/static/tiktok/package.json +3 -0
- package/static/tiktok/shop_bsid/core.js +1 -0
- package/static/tiktok/shop_bsid/env_core.js +512 -0
- package/static/tiktok/shop_bsid/loader.js +1 -0
- package/static/tiktok/shop_bsid/run.js +753 -0
- package/static/tiktok/shop_bsid/sign.js +100 -0
- package/static/tiktok/shop_bsid/webmssdk.js +1 -0
- package/static/tiktok/shop_bsid/webmssdk_ex.js +1 -0
- package/static/tiktok/signing/env/env_core.js +512 -0
- package/static/tiktok/signing/env/sign.js +455 -0
- package/static/tiktok/signing/sdk_config.json +230 -0
- package/static/tiktok/signing/source/webmssdk.js +1 -0
- package/static/tiktok/signing/source/webmssdk_ex.js +1 -0
- package/static/tiktok/signing/source/webmssdk_legacy.js +1 -0
- package/static/x/graphql.json +7777 -0
- package/static/x/transaction_l1.json +12 -0
- package/static/xhs/package.json +3 -0
- package/static/xhs/xhs_core/js/b1.js +210 -0
- package/static/xhs/xhs_core/js/mns.js +340 -0
- package/static/xhs/xhs_core/js/mns_0101_keystream.json +5 -0
- package/static/xhs/xhs_core/js/mns_keystreams.json +1 -0
- package/static/xhs/xhs_core/js/sign.js +225 -0
- package/static/xhs/xhs_core/js/websectiga_cli.js +220 -0
- package/static/xhs/xhs_core/js/websectiga_env.js +512 -0
- package/static/xhs/xhs_core/js/xs_common.js +79 -0
- package/static/xhs/xhs_creator/js/profile.js +115 -0
- package/static/xhs/xhs_creator/js/rap_fingerprint_creator.json +6 -0
- package/static/xhs/xhs_creator/js/reference_profile.json +282 -0
- package/static/xhs/xhs_creator/js/xhs_creator_sign.js +29 -0
- package/static/xhs/xhs_creator/js/xhs_creator_signature.js +23 -0
- package/static/xhs/xhs_pc/js/aes_decrypt.js +22 -0
- package/static/xhs/xhs_pc/js/aes_encrypt.js +30 -0
- package/static/xhs/xhs_pc/js/b1.js +9 -0
- package/static/xhs/xhs_pc/js/deflate.js +320 -0
- package/static/xhs/xhs_pc/js/mns.js +4 -0
- package/static/xhs/xhs_pc/js/profile.js +141 -0
- package/static/xhs/xhs_pc/js/rap.js +173 -0
- package/static/xhs/xhs_pc/js/rap_cli.js +30 -0
- package/static/xhs/xhs_pc/js/rap_crypto.js +73 -0
- package/static/xhs/xhs_pc/js/rap_fingerprint_template.json +8 -0
- package/static/xhs/xhs_pc/js/reference_profile.json +262 -0
- package/static/xhs/xhs_pc/js/sign.js +6 -0
- package/static/xhs/xhs_pc/js/web_ssk.js +82 -0
- package/static/xhs/xhs_pc/js/xs_common.js +4 -0
- package/static/xianyu/et_f.js +2 -0
- package/static/xianyu/gen_tfstk.js +410 -0
- package/static/xianyu/goofish_js_version_2.js +568 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { SpecificAIError } from '../../../core/errors.js';
|
|
3
|
+
import { decodeImage as decodeRgba, loadCv } from '../../../core/image.js';
|
|
4
|
+
const f = Math.fround;
|
|
5
|
+
// ---------------------------------------------------------------- 图片
|
|
6
|
+
/** 解码题图为 RGB(alpha 丢弃,与 cv2.imdecode 的 IMREAD_COLOR 一致)。 */
|
|
7
|
+
export async function decodeImage(bytes) {
|
|
8
|
+
const png = await decodeRgba(bytes);
|
|
9
|
+
if (!png)
|
|
10
|
+
throw new SpecificAIError('RISK_CONTROL', '极验题图无法解码', { detail: { kind: 'captcha' } });
|
|
11
|
+
const data = new Uint8Array(png.width * png.height * 3);
|
|
12
|
+
for (let i = 0, j = 0; j < data.length; i += 4, j += 3)
|
|
13
|
+
data.set(png.rgba.subarray(i, i + 3), j);
|
|
14
|
+
return { width: png.width, height: png.height, data };
|
|
15
|
+
}
|
|
16
|
+
/** PIL 的 `Image.crop((x1, y1, x2, y2))`:越界部分填 0。 */
|
|
17
|
+
export function crop(im, x1, y1, x2, y2) {
|
|
18
|
+
const w = Math.max(0, x2 - x1);
|
|
19
|
+
const h = Math.max(0, y2 - y1);
|
|
20
|
+
const data = new Uint8Array(w * h * 3);
|
|
21
|
+
for (let y = 0; y < h; y++) {
|
|
22
|
+
const sy = y + y1;
|
|
23
|
+
if (sy < 0 || sy >= im.height)
|
|
24
|
+
continue;
|
|
25
|
+
for (let x = 0; x < w; x++) {
|
|
26
|
+
const sx = x + x1;
|
|
27
|
+
if (sx < 0 || sx >= im.width)
|
|
28
|
+
continue;
|
|
29
|
+
const s = (sy * im.width + sx) * 3;
|
|
30
|
+
data.set(im.data.subarray(s, s + 3), (y * w + x) * 3);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { width: w, height: h, data };
|
|
34
|
+
}
|
|
35
|
+
// ---------------------------------------------------------------- Pillow:LANCZOS 缩放与转灰度
|
|
36
|
+
const PRECISION_BITS = 32 - 8 - 2;
|
|
37
|
+
function sinc(x) {
|
|
38
|
+
if (x === 0)
|
|
39
|
+
return 1;
|
|
40
|
+
x *= Math.PI;
|
|
41
|
+
return Math.sin(x) / x;
|
|
42
|
+
}
|
|
43
|
+
const lanczos = (x) => (-3 <= x && x < 3 ? sinc(x) * sinc(x / 3) : 0);
|
|
44
|
+
/** Resample.c 的 precompute_coeffs + normalize_coeffs_8bpc。 */
|
|
45
|
+
function coeffs(inSize, outSize) {
|
|
46
|
+
const scale = inSize / outSize;
|
|
47
|
+
const filterscale = scale < 1 ? 1 : scale;
|
|
48
|
+
const support = 3 * filterscale;
|
|
49
|
+
const ksize = Math.ceil(support) * 2 + 1;
|
|
50
|
+
const bounds = new Int32Array(outSize * 2);
|
|
51
|
+
const kk = new Int32Array(outSize * ksize);
|
|
52
|
+
const k = new Float64Array(ksize);
|
|
53
|
+
const inv = 1 / filterscale;
|
|
54
|
+
for (let xx = 0; xx < outSize; xx++) {
|
|
55
|
+
const center = (xx + 0.5) * scale;
|
|
56
|
+
let ww = 0;
|
|
57
|
+
const xmin = Math.max(0, Math.trunc(center - support + 0.5));
|
|
58
|
+
const xmax = Math.min(inSize, Math.trunc(center + support + 0.5)) - xmin;
|
|
59
|
+
k.fill(0);
|
|
60
|
+
for (let x = 0; x < xmax; x++) {
|
|
61
|
+
const w = lanczos((x + xmin - center + 0.5) * inv);
|
|
62
|
+
k[x] = w;
|
|
63
|
+
ww += w;
|
|
64
|
+
}
|
|
65
|
+
if (ww !== 0)
|
|
66
|
+
for (let x = 0; x < xmax; x++)
|
|
67
|
+
k[x] /= ww;
|
|
68
|
+
for (let x = 0; x < ksize; x++) {
|
|
69
|
+
const v = k[x] * (1 << PRECISION_BITS);
|
|
70
|
+
kk[xx * ksize + x] = Math.trunc(v < 0 ? v - 0.5 : v + 0.5);
|
|
71
|
+
}
|
|
72
|
+
bounds[xx * 2] = xmin;
|
|
73
|
+
bounds[xx * 2 + 1] = xmax;
|
|
74
|
+
}
|
|
75
|
+
return { ksize, bounds, kk };
|
|
76
|
+
}
|
|
77
|
+
const clip8 = (ss) => Math.min(255, Math.max(0, Math.floor(ss / (1 << PRECISION_BITS))));
|
|
78
|
+
/** `Image.resize((w, h), Image.LANCZOS)`(ImagingResampleInner:先水平、后垂直,水平只处理垂直方向用得到的行)。 */
|
|
79
|
+
export function resizeLanczos(im, width, height) {
|
|
80
|
+
if (width === im.width && height === im.height)
|
|
81
|
+
return { width, height, data: im.data.slice() };
|
|
82
|
+
const needH = width !== im.width;
|
|
83
|
+
const needV = height !== im.height;
|
|
84
|
+
const vert = coeffs(im.height, height);
|
|
85
|
+
let src = im;
|
|
86
|
+
if (needH) {
|
|
87
|
+
const first = vert.bounds[0];
|
|
88
|
+
const last = vert.bounds[height * 2 - 2] + vert.bounds[height * 2 - 1];
|
|
89
|
+
for (let i = 0; i < height; i++)
|
|
90
|
+
vert.bounds[i * 2] -= first;
|
|
91
|
+
const { ksize, bounds, kk } = coeffs(im.width, width);
|
|
92
|
+
const rows = last - first;
|
|
93
|
+
const out = new Uint8Array(width * rows * 3);
|
|
94
|
+
for (let yy = 0; yy < rows; yy++) {
|
|
95
|
+
const line = (yy + first) * im.width * 3;
|
|
96
|
+
for (let xx = 0; xx < width; xx++) {
|
|
97
|
+
const xmin = bounds[xx * 2];
|
|
98
|
+
const xmax = bounds[xx * 2 + 1];
|
|
99
|
+
const k = xx * ksize;
|
|
100
|
+
let s0 = 1 << (PRECISION_BITS - 1);
|
|
101
|
+
let s1 = s0;
|
|
102
|
+
let s2 = s0;
|
|
103
|
+
for (let x = 0; x < xmax; x++) {
|
|
104
|
+
const p = line + (x + xmin) * 3;
|
|
105
|
+
const c = kk[k + x];
|
|
106
|
+
s0 += im.data[p] * c;
|
|
107
|
+
s1 += im.data[p + 1] * c;
|
|
108
|
+
s2 += im.data[p + 2] * c;
|
|
109
|
+
}
|
|
110
|
+
const o = (yy * width + xx) * 3;
|
|
111
|
+
out[o] = clip8(s0);
|
|
112
|
+
out[o + 1] = clip8(s1);
|
|
113
|
+
out[o + 2] = clip8(s2);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
src = { width, height: rows, data: out };
|
|
117
|
+
}
|
|
118
|
+
if (!needV)
|
|
119
|
+
return src;
|
|
120
|
+
const { ksize, bounds, kk } = vert;
|
|
121
|
+
const out = new Uint8Array(src.width * height * 3);
|
|
122
|
+
for (let yy = 0; yy < height; yy++) {
|
|
123
|
+
const ymin = bounds[yy * 2];
|
|
124
|
+
const ymax = bounds[yy * 2 + 1];
|
|
125
|
+
const k = yy * ksize;
|
|
126
|
+
for (let xx = 0; xx < src.width; xx++) {
|
|
127
|
+
let s0 = 1 << (PRECISION_BITS - 1);
|
|
128
|
+
let s1 = s0;
|
|
129
|
+
let s2 = s0;
|
|
130
|
+
for (let y = 0; y < ymax; y++) {
|
|
131
|
+
const p = ((y + ymin) * src.width + xx) * 3;
|
|
132
|
+
const c = kk[k + y];
|
|
133
|
+
s0 += src.data[p] * c;
|
|
134
|
+
s1 += src.data[p + 1] * c;
|
|
135
|
+
s2 += src.data[p + 2] * c;
|
|
136
|
+
}
|
|
137
|
+
const o = (yy * src.width + xx) * 3;
|
|
138
|
+
out[o] = clip8(s0);
|
|
139
|
+
out[o + 1] = clip8(s1);
|
|
140
|
+
out[o + 2] = clip8(s2);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { width: src.width, height, data: out };
|
|
144
|
+
}
|
|
145
|
+
/** `convert('L')`:ITU-R 601-2,Pillow 的定点写法 L24。 */
|
|
146
|
+
export function toGray(im) {
|
|
147
|
+
const out = new Uint8Array(im.width * im.height);
|
|
148
|
+
for (let i = 0; i < out.length; i++) {
|
|
149
|
+
const p = i * 3;
|
|
150
|
+
out[i] = (im.data[p] * 19595 + im.data[p + 1] * 38470 + im.data[p + 2] * 7471 + 0x8000) >> 16;
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
/** 识别模型的输入:等比缩放到高 64 后转灰度(OCREngine._preprocess_image)。 */
|
|
155
|
+
export function ocrInput(im) {
|
|
156
|
+
const width = Math.trunc(im.width * (64 / im.height));
|
|
157
|
+
return { width, height: 64, gray: toGray(resizeLanczos(im, width, 64)) };
|
|
158
|
+
}
|
|
159
|
+
let modelPaths = null;
|
|
160
|
+
function models() {
|
|
161
|
+
modelPaths ??= import('specific-ai-assets-ocr').then((m) => m.models, () => {
|
|
162
|
+
modelPaths = null;
|
|
163
|
+
throw new SpecificAIError('ERROR', '没有安装 OCR 模型包 specific-ai-assets-ocr', { hint: '重新安装 specific-ai-cli;specific-ai doctor 可以检查' });
|
|
164
|
+
});
|
|
165
|
+
return modelPaths;
|
|
166
|
+
}
|
|
167
|
+
const sessions = new Map();
|
|
168
|
+
async function session(which) {
|
|
169
|
+
let s = sessions.get(which);
|
|
170
|
+
if (!s) {
|
|
171
|
+
s = (async () => {
|
|
172
|
+
const path = (await models())[which];
|
|
173
|
+
let bytes;
|
|
174
|
+
try {
|
|
175
|
+
bytes = readFileSync(path);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
throw new SpecificAIError('ERROR', `OCR 模型文件缺失:${path}`, { hint: '重新安装 specific-ai-cli(specific-ai-assets-ocr)' });
|
|
179
|
+
}
|
|
180
|
+
const ort = await import('onnxruntime-web');
|
|
181
|
+
ort.env.wasm.numThreads = 1;
|
|
182
|
+
// 识别模型声明的输出形状不对,每次推理都会警告;与上游 set_default_logger_severity(3) 一样只报错误
|
|
183
|
+
ort.env.logLevel = 'error';
|
|
184
|
+
return ort.InferenceSession.create(bytes, { logSeverityLevel: 3 });
|
|
185
|
+
})();
|
|
186
|
+
s.catch(() => sessions.delete(which));
|
|
187
|
+
sessions.set(which, s);
|
|
188
|
+
}
|
|
189
|
+
return s;
|
|
190
|
+
}
|
|
191
|
+
let charsetCache = null;
|
|
192
|
+
async function charset() {
|
|
193
|
+
charsetCache ??= models().then((m) => JSON.parse(readFileSync(m.charset, 'utf8')));
|
|
194
|
+
return charsetCache;
|
|
195
|
+
}
|
|
196
|
+
async function run(which, data, dims) {
|
|
197
|
+
const ort = await import('onnxruntime-web');
|
|
198
|
+
const sess = await session(which);
|
|
199
|
+
const out = await sess.run({ [sess.inputNames[0]]: new ort.Tensor('float32', data, dims) });
|
|
200
|
+
const t = out[sess.outputNames[0]];
|
|
201
|
+
return { data: t.data, dims: t.dims };
|
|
202
|
+
}
|
|
203
|
+
// ---------------------------------------------------------------- 检测
|
|
204
|
+
const DET_SIZE = 416;
|
|
205
|
+
/** DetectionEngine.preproc:BGR 等比缩放贴到 114 灰底的 416×416(HWC uint8)。 */
|
|
206
|
+
export async function detInput(im) {
|
|
207
|
+
const cv = await loadCv();
|
|
208
|
+
const r = Math.min(DET_SIZE / im.height, DET_SIZE / im.width);
|
|
209
|
+
const w = Math.trunc(im.width * r);
|
|
210
|
+
const h = Math.trunc(im.height * r);
|
|
211
|
+
const bgr = new Uint8Array(im.data.length);
|
|
212
|
+
for (let i = 0; i < bgr.length; i += 3) {
|
|
213
|
+
bgr[i] = im.data[i + 2];
|
|
214
|
+
bgr[i + 1] = im.data[i + 1];
|
|
215
|
+
bgr[i + 2] = im.data[i];
|
|
216
|
+
}
|
|
217
|
+
const src = new cv.Mat(im.height, im.width, cv.CV_8UC3);
|
|
218
|
+
const dst = new cv.Mat();
|
|
219
|
+
let resized;
|
|
220
|
+
try {
|
|
221
|
+
src.data.set(bgr);
|
|
222
|
+
cv.resize(src, dst, new cv.Size(w, h), 0, 0, cv.INTER_LINEAR);
|
|
223
|
+
resized = new Uint8Array(dst.data);
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
src.delete();
|
|
227
|
+
dst.delete();
|
|
228
|
+
}
|
|
229
|
+
const padded = new Uint8Array(DET_SIZE * DET_SIZE * 3).fill(114);
|
|
230
|
+
for (let y = 0; y < h; y++)
|
|
231
|
+
padded.set(resized.subarray(y * w * 3, (y + 1) * w * 3), y * DET_SIZE * 3);
|
|
232
|
+
return { ratio: r, padded };
|
|
233
|
+
}
|
|
234
|
+
/** 单类 NMS(DetectionEngine.nms),float32 语义。 */
|
|
235
|
+
function nms(boxes, scores, thr) {
|
|
236
|
+
const area = boxes.map(([x1, y1, x2, y2]) => f(f(f(x2 - x1) + 1) * f(f(y2 - y1) + 1)));
|
|
237
|
+
// scores.argsort()[::-1]:升序后倒过来
|
|
238
|
+
let order = scores
|
|
239
|
+
.map((s, i) => [s, i])
|
|
240
|
+
.sort((a, b) => a[0] - b[0] || a[1] - b[1])
|
|
241
|
+
.map(([, i]) => i)
|
|
242
|
+
.reverse();
|
|
243
|
+
const keep = [];
|
|
244
|
+
const t = f(thr);
|
|
245
|
+
while (order.length) {
|
|
246
|
+
const i = order[0];
|
|
247
|
+
keep.push(i);
|
|
248
|
+
const [ax1, ay1, ax2, ay2] = boxes[i];
|
|
249
|
+
order = order.slice(1).filter((j) => {
|
|
250
|
+
const [bx1, by1, bx2, by2] = boxes[j];
|
|
251
|
+
const w = Math.max(0, f(f(Math.min(ax2, bx2) - Math.max(ax1, bx1)) + 1));
|
|
252
|
+
const h = Math.max(0, f(f(Math.min(ay2, by2) - Math.max(ay1, by1)) + 1));
|
|
253
|
+
const inter = f(w * h);
|
|
254
|
+
return f(inter / f(f(area[i] + area[j]) - inter)) <= t;
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return keep;
|
|
258
|
+
}
|
|
259
|
+
/** 检测字符框(DetectionEngine.get_bbox),返回 [x1, y1, x2, y2] 整数框,顺序同上游(按分数)。 */
|
|
260
|
+
export async function detect(im) {
|
|
261
|
+
const { ratio, padded } = await detInput(im);
|
|
262
|
+
const plane = DET_SIZE * DET_SIZE;
|
|
263
|
+
const tensor = new Float32Array(3 * plane);
|
|
264
|
+
for (let i = 0; i < plane; i++)
|
|
265
|
+
for (let c = 0; c < 3; c++)
|
|
266
|
+
tensor[c * plane + i] = padded[i * 3 + c];
|
|
267
|
+
const { data, dims } = await run('det', tensor, [1, 3, DET_SIZE, DET_SIZE]);
|
|
268
|
+
const n = dims[1];
|
|
269
|
+
const width = dims[2];
|
|
270
|
+
const boxes = [];
|
|
271
|
+
const scores = [];
|
|
272
|
+
const thr = f(0.1);
|
|
273
|
+
const r = f(ratio);
|
|
274
|
+
let a = 0;
|
|
275
|
+
for (const stride of [8, 16, 32]) {
|
|
276
|
+
const size = DET_SIZE / stride;
|
|
277
|
+
for (let gy = 0; gy < size; gy++) {
|
|
278
|
+
for (let gx = 0; gx < size; gx++, a++) {
|
|
279
|
+
if (a >= n)
|
|
280
|
+
break;
|
|
281
|
+
const o = a * width;
|
|
282
|
+
const cx = f((data[o] + gx) * stride);
|
|
283
|
+
const cy = f((data[o + 1] + gy) * stride);
|
|
284
|
+
const w = f(f(Math.exp(data[o + 2])) * stride);
|
|
285
|
+
const h = f(f(Math.exp(data[o + 3])) * stride);
|
|
286
|
+
const score = f(data[o + 4] * data[o + 5]);
|
|
287
|
+
if (!(score > thr))
|
|
288
|
+
continue;
|
|
289
|
+
const hw = f(w / 2);
|
|
290
|
+
const hh = f(h / 2);
|
|
291
|
+
boxes.push([f(f(cx - hw) / r), f(f(cy - hh) / r), f(f(cx + hw) / r), f(f(cy + hh) / r)]);
|
|
292
|
+
scores.push(score);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (!boxes.length)
|
|
297
|
+
return [];
|
|
298
|
+
return nms(boxes, scores, 0.45).map((i) => {
|
|
299
|
+
const [x1, y1, x2, y2] = boxes[i];
|
|
300
|
+
return [
|
|
301
|
+
x1 < 0 ? 0 : Math.trunc(x1),
|
|
302
|
+
y1 < 0 ? 0 : Math.trunc(y1),
|
|
303
|
+
x2 > im.width ? im.width : Math.trunc(x2),
|
|
304
|
+
y2 > im.height ? im.height : Math.trunc(y2),
|
|
305
|
+
];
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
/** `classification(img, probability=True)`:灰度识别,返回文字和每一步的字符概率。 */
|
|
309
|
+
export async function classify(im) {
|
|
310
|
+
const { width, height, gray } = ocrInput(im);
|
|
311
|
+
const input = new Float32Array(gray.length);
|
|
312
|
+
for (let i = 0; i < gray.length; i++)
|
|
313
|
+
input[i] = f(gray[i] / 255);
|
|
314
|
+
const { data, dims } = await run('ocr', input, [1, 1, height, width]);
|
|
315
|
+
// 输出是 [T, 1, C](模型里声明的形状不对,按实际的来)
|
|
316
|
+
const classes = dims[dims.length - 1];
|
|
317
|
+
const steps = data.length / classes;
|
|
318
|
+
const cs = await charset();
|
|
319
|
+
const probabilities = new Float32Array(data.length);
|
|
320
|
+
let text = '';
|
|
321
|
+
let prev = -1;
|
|
322
|
+
for (let t = 0; t < steps; t++) {
|
|
323
|
+
const row = data.subarray(t * classes, (t + 1) * classes);
|
|
324
|
+
let max = -Infinity;
|
|
325
|
+
let arg = 0;
|
|
326
|
+
for (let c = 0; c < classes; c++) {
|
|
327
|
+
if (row[c] > max) {
|
|
328
|
+
max = row[c];
|
|
329
|
+
arg = c;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
let sum = 0;
|
|
333
|
+
for (let c = 0; c < classes; c++)
|
|
334
|
+
sum += Math.exp(row[c] - max);
|
|
335
|
+
for (let c = 0; c < classes; c++)
|
|
336
|
+
probabilities[t * classes + c] = f(Math.exp(row[c] - max) / sum);
|
|
337
|
+
if (arg !== prev && arg !== 0 && arg < cs.length)
|
|
338
|
+
text += cs[arg];
|
|
339
|
+
prev = arg;
|
|
340
|
+
}
|
|
341
|
+
return { text, probabilities, steps, classes };
|
|
342
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { quote } from '../../../core/py.js';
|
|
2
|
+
import * as rand from '../../../core/rand.js';
|
|
3
|
+
import { PROFILE } from './profile.js';
|
|
4
|
+
/**
|
|
5
|
+
* 风控指纹(上游 apis/bili_gaia_apis.py)。只移植明文上报 `ExClimbWuzhi` 这一路:
|
|
6
|
+
* 加密上报依赖 CDN 上的第三方 SDK,上游也默认不发,已实证写接口不受影响。
|
|
7
|
+
*/
|
|
8
|
+
const PLUGINS = [
|
|
9
|
+
'PDF Viewer',
|
|
10
|
+
'Chrome PDF Viewer',
|
|
11
|
+
'Chromium PDF Viewer',
|
|
12
|
+
'Microsoft Edge PDF Viewer',
|
|
13
|
+
'WebKit built-in PDF',
|
|
14
|
+
].map((name) => [name, 'Portable Document Format', [['application/pdf', 'pdf'], ['text/pdf', 'pdf']]]);
|
|
15
|
+
/** canvas 指纹尾串,同时是 qrcode/poll 的 b_ret。 */
|
|
16
|
+
export const CANVAS_1 = 'mW9qAAAAAElFTkSuQmCC';
|
|
17
|
+
const CANVAS_2 = '//TgNIfAAAAAZJREFUAwBde+3wgcxEHQAAAABJRU5ErkJggg==';
|
|
18
|
+
const AUDIO_FP = '124.04347527516074';
|
|
19
|
+
const PRECISIONS = ['vertex', 'fragment'].flatMap((shader) => ['high', 'medium', 'low'].flatMap((p) => [
|
|
20
|
+
`webgl ${shader} shader ${p} float precision:23`,
|
|
21
|
+
`webgl ${shader} shader ${p} float precision rangeMin:127`,
|
|
22
|
+
`webgl ${shader} shader ${p} float precision rangeMax:127`,
|
|
23
|
+
]));
|
|
24
|
+
const INT_PRECISIONS = ['vertex', 'fragment'].flatMap((shader) => ['high', 'medium', 'low'].flatMap((p) => [
|
|
25
|
+
`webgl ${shader} shader ${p} int precision:0`,
|
|
26
|
+
`webgl ${shader} shader ${p} int precision rangeMin:31`,
|
|
27
|
+
`webgl ${shader} shader ${p} int precision rangeMax:30`,
|
|
28
|
+
]));
|
|
29
|
+
const WEBGL_PARAMS = [
|
|
30
|
+
'extensions:ANGLE_instanced_arrays;EXT_blend_minmax;EXT_clip_control;' +
|
|
31
|
+
'EXT_color_buffer_half_float;EXT_depth_clamp;EXT_disjoint_timer_query;' +
|
|
32
|
+
'EXT_float_blend;EXT_frag_depth;EXT_polygon_offset_clamp;' +
|
|
33
|
+
'EXT_shader_texture_lod;EXT_texture_compression_bptc;' +
|
|
34
|
+
'EXT_texture_compression_rgtc;EXT_texture_filter_anisotropic;' +
|
|
35
|
+
'EXT_texture_mirror_clamp_to_edge;EXT_sRGB;KHR_parallel_shader_compile;' +
|
|
36
|
+
'OES_element_index_uint;OES_fbo_render_mipmap;OES_standard_derivatives;' +
|
|
37
|
+
'OES_texture_float;OES_texture_float_linear;OES_texture_half_float;' +
|
|
38
|
+
'OES_texture_half_float_linear;OES_vertex_array_object;' +
|
|
39
|
+
'WEBGL_blend_func_extended;WEBGL_color_buffer_float;' +
|
|
40
|
+
'WEBGL_compressed_texture_s3tc;WEBGL_compressed_texture_s3tc_srgb;' +
|
|
41
|
+
'WEBGL_debug_renderer_info;WEBGL_debug_shaders;WEBGL_depth_texture;' +
|
|
42
|
+
'WEBGL_draw_buffers;WEBGL_lose_context;WEBGL_multi_draw;' +
|
|
43
|
+
'WEBGL_polygon_mode',
|
|
44
|
+
'webgl aliased line width range:[1, 1]',
|
|
45
|
+
'webgl aliased point size range:[1, 1024]',
|
|
46
|
+
'webgl alpha bits:8',
|
|
47
|
+
'webgl antialiasing:yes',
|
|
48
|
+
'webgl blue bits:8',
|
|
49
|
+
'webgl depth bits:24',
|
|
50
|
+
'webgl green bits:8',
|
|
51
|
+
'webgl max anisotropy:16',
|
|
52
|
+
'webgl max combined texture image units:32',
|
|
53
|
+
'webgl max cube map texture size:16384',
|
|
54
|
+
'webgl max fragment uniform vectors:1024',
|
|
55
|
+
'webgl max render buffer size:16384',
|
|
56
|
+
'webgl max texture image units:16',
|
|
57
|
+
'webgl max texture size:16384',
|
|
58
|
+
'webgl max varying vectors:30',
|
|
59
|
+
'webgl max vertex attribs:16',
|
|
60
|
+
'webgl max vertex texture image units:16',
|
|
61
|
+
'webgl max vertex uniform vectors:4095',
|
|
62
|
+
'webgl max viewport dims:[32767, 32767]',
|
|
63
|
+
'webgl red bits:8',
|
|
64
|
+
'webgl renderer:WebKit WebGL',
|
|
65
|
+
'webgl shading language version:WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)',
|
|
66
|
+
'webgl stencil bits:0',
|
|
67
|
+
'webgl vendor:WebKit',
|
|
68
|
+
'webgl version:WebGL 1.0 (OpenGL ES 2.0 Chromium)',
|
|
69
|
+
'webgl unmasked vendor:Google Inc. (NVIDIA)',
|
|
70
|
+
'webgl unmasked renderer:ANGLE (NVIDIA, NVIDIA GeForce RTX 5060 Ti (0x00002D04) Direct3D11 vs_5_0 ps_5_0, D3D11)',
|
|
71
|
+
...PRECISIONS,
|
|
72
|
+
...INT_PRECISIONS,
|
|
73
|
+
];
|
|
74
|
+
/** `3c43` 指纹主体,也是 buvid_fp 的 murmur3 输入。键序照抄实抓。 */
|
|
75
|
+
export function fingerCore() {
|
|
76
|
+
const w = PROFILE.screenWidth;
|
|
77
|
+
const h = PROFILE.screenHeight;
|
|
78
|
+
return new Map([
|
|
79
|
+
['2673', 0],
|
|
80
|
+
['5766', PROFILE.colorDepth],
|
|
81
|
+
['6527', 0],
|
|
82
|
+
['7003', 1],
|
|
83
|
+
['807e', 1],
|
|
84
|
+
['b8ce', PROFILE.ua],
|
|
85
|
+
['641c', 0],
|
|
86
|
+
['07a4', 'zh-CN'],
|
|
87
|
+
['1c57', 32],
|
|
88
|
+
['0bd0', PROFILE.hardwareConcurrency],
|
|
89
|
+
['748e', [w, h]],
|
|
90
|
+
['d61f', [w, h - 48]],
|
|
91
|
+
['fc9d', -480],
|
|
92
|
+
['6aa9', PROFILE.timezone],
|
|
93
|
+
['75b8', 1],
|
|
94
|
+
['3b21', 1],
|
|
95
|
+
['8a1c', 0],
|
|
96
|
+
['d52f', 'not available'],
|
|
97
|
+
['adca', 'Win32'],
|
|
98
|
+
['80c9', PLUGINS],
|
|
99
|
+
['13ab', CANVAS_1],
|
|
100
|
+
['bfe9', CANVAS_2],
|
|
101
|
+
['a3c1', WEBGL_PARAMS],
|
|
102
|
+
['6bc5', `${PROFILE.webglVendor}~${PROFILE.webglRenderer}`],
|
|
103
|
+
['ed31', 0],
|
|
104
|
+
['72bd', 0],
|
|
105
|
+
['097b', 0],
|
|
106
|
+
['52cd', [10, 0, 0]],
|
|
107
|
+
['a658', []],
|
|
108
|
+
['d02f', AUDIO_FP],
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
/** ExClimbWuzhi 的 payload 对象。 */
|
|
112
|
+
export function fingerPayload(uuid, referer = 'https://www.bilibili.com/', correspondUrl = '') {
|
|
113
|
+
return new Map([
|
|
114
|
+
['3064', 1],
|
|
115
|
+
['5062', String(rand.now())],
|
|
116
|
+
['03bf', quote(correspondUrl, '')],
|
|
117
|
+
['39c8', '333.1193.fp.risk'],
|
|
118
|
+
['34f1', ''],
|
|
119
|
+
['d402', ''],
|
|
120
|
+
['654a', ''],
|
|
121
|
+
['6e7c', '0x0'],
|
|
122
|
+
['3c43', fingerCore()],
|
|
123
|
+
['54ef', '{}'],
|
|
124
|
+
['8b94', quote(referer, '')],
|
|
125
|
+
['df35', uuid],
|
|
126
|
+
['07a4', 'zh-CN'],
|
|
127
|
+
['5f45', null],
|
|
128
|
+
['db46', 0],
|
|
129
|
+
]);
|
|
130
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { SpecificAIError } from '../../../core/errors.js';
|
|
3
|
+
import { interactive } from '../../../core/login.js';
|
|
4
|
+
/** 上游默认端口;被占用时换一个空闲端口。 */
|
|
5
|
+
const PORT = 8777;
|
|
6
|
+
/** 秒。 */
|
|
7
|
+
const TIMEOUT = 300;
|
|
8
|
+
function page(gt, challenge) {
|
|
9
|
+
return `<!doctype html>
|
|
10
|
+
<html lang="zh-CN">
|
|
11
|
+
<head>
|
|
12
|
+
<meta charset="utf-8">
|
|
13
|
+
<title>B站登录 - 人机验证</title>
|
|
14
|
+
<script src="https://static.geetest.com/static/tools/gt.js"></script>
|
|
15
|
+
<style>
|
|
16
|
+
body { font-family: -apple-system, "Microsoft YaHei", sans-serif; background:#f6f7f9;
|
|
17
|
+
display:flex; align-items:center; justify-content:center; height:100vh; margin:0; }
|
|
18
|
+
.card { background:#fff; padding:32px 40px; border-radius:12px; text-align:center;
|
|
19
|
+
box-shadow:0 8px 30px rgba(0,0,0,.08); min-width:360px; }
|
|
20
|
+
h1 { font-size:18px; margin:0 0 6px; color:#18191c; }
|
|
21
|
+
p { font-size:13px; color:#9499a0; margin:0 0 20px; }
|
|
22
|
+
#done { display:none; color:#00a1d6; font-size:15px; font-weight:600; margin-top:16px; }
|
|
23
|
+
</style>
|
|
24
|
+
</head>
|
|
25
|
+
<body>
|
|
26
|
+
<div class="card">
|
|
27
|
+
<h1>完成人机验证</h1>
|
|
28
|
+
<p>验证通过后本页会自动回传结果,可直接关闭</p>
|
|
29
|
+
<div id="captcha"></div>
|
|
30
|
+
<div id="done">验证已通过,结果已回传,可以关闭本页</div>
|
|
31
|
+
</div>
|
|
32
|
+
<script>
|
|
33
|
+
initGeetest({
|
|
34
|
+
gt: ${JSON.stringify(gt)},
|
|
35
|
+
challenge: ${JSON.stringify(challenge)},
|
|
36
|
+
offline: false,
|
|
37
|
+
new_captcha: true,
|
|
38
|
+
product: "bind",
|
|
39
|
+
https: true
|
|
40
|
+
}, function (captchaObj) {
|
|
41
|
+
captchaObj.onReady(function () { captchaObj.verify(); });
|
|
42
|
+
captchaObj.onSuccess(function () {
|
|
43
|
+
var r = captchaObj.getValidate();
|
|
44
|
+
window.__geetest_result = r;
|
|
45
|
+
fetch("/result", { method: "POST", body: JSON.stringify(r) });
|
|
46
|
+
document.getElementById("done").style.display = "block";
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
</script>
|
|
50
|
+
</body>
|
|
51
|
+
</html>
|
|
52
|
+
`;
|
|
53
|
+
}
|
|
54
|
+
function listen(server, port) {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const onError = (err) => {
|
|
57
|
+
server.off('listening', onListening);
|
|
58
|
+
reject(err);
|
|
59
|
+
};
|
|
60
|
+
const onListening = () => {
|
|
61
|
+
server.off('error', onError);
|
|
62
|
+
resolve(server.address().port);
|
|
63
|
+
};
|
|
64
|
+
server.once('error', onError);
|
|
65
|
+
server.once('listening', onListening);
|
|
66
|
+
server.listen(port, '127.0.0.1');
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function readBody(req) {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const chunks = [];
|
|
72
|
+
req.on('data', (c) => chunks.push(c));
|
|
73
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
74
|
+
req.on('error', reject);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** 起本地验证页面(不检查 TTY,也不打印),返回地址和等待结果的 Promise。 */
|
|
78
|
+
export async function serveManual(gt, challenge, options = {}) {
|
|
79
|
+
const timeout = options.timeout ?? TIMEOUT;
|
|
80
|
+
let current = {};
|
|
81
|
+
let settle;
|
|
82
|
+
const result = new Promise((resolve, reject) => (settle = { resolve, reject }));
|
|
83
|
+
result.catch(() => { });
|
|
84
|
+
const json = (res, status, payload) => {
|
|
85
|
+
const body = Buffer.from(JSON.stringify(payload));
|
|
86
|
+
res.writeHead(status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Content-Length': body.length });
|
|
87
|
+
res.end(body);
|
|
88
|
+
};
|
|
89
|
+
const server = createServer(async (req, res) => {
|
|
90
|
+
const path = new URL(req.url ?? '/', 'http://127.0.0.1').pathname;
|
|
91
|
+
if (req.method === 'POST') {
|
|
92
|
+
let r = {};
|
|
93
|
+
try {
|
|
94
|
+
r = JSON.parse((await readBody(req)) || '{}');
|
|
95
|
+
}
|
|
96
|
+
catch { }
|
|
97
|
+
const validate = typeof r.geetest_validate === 'string' ? r.geetest_validate : '';
|
|
98
|
+
if (!validate)
|
|
99
|
+
return json(res, 400, { ok: false });
|
|
100
|
+
current = r;
|
|
101
|
+
json(res, 200, { ok: true });
|
|
102
|
+
settle.resolve({
|
|
103
|
+
challenge: typeof r.geetest_challenge === 'string' && r.geetest_challenge ? r.geetest_challenge : challenge,
|
|
104
|
+
validate,
|
|
105
|
+
seccode: typeof r.geetest_seccode === 'string' && r.geetest_seccode ? r.geetest_seccode : `${validate}|jordan`,
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (path === '/poll')
|
|
110
|
+
return json(res, 200, current);
|
|
111
|
+
// 每个 challenge 只能用一次,页面被缓存会报 GeetestError: old challenge
|
|
112
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store, no-cache, must-revalidate', Pragma: 'no-cache' });
|
|
113
|
+
res.end(page(gt, challenge));
|
|
114
|
+
});
|
|
115
|
+
let port;
|
|
116
|
+
try {
|
|
117
|
+
port = await listen(server, options.port ?? PORT);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
if (err.code !== 'EADDRINUSE' || options.port === 0)
|
|
121
|
+
throw err;
|
|
122
|
+
port = await listen(server, 0);
|
|
123
|
+
}
|
|
124
|
+
const timer = setTimeout(() => settle.reject(new SpecificAIError('RISK_CONTROL', `人机验证超时:${timeout} 秒内没有在浏览器里完成`, { hint: '重新执行命令', detail: { kind: 'captcha' } })), timeout * 1000);
|
|
125
|
+
const onAbort = () => settle.reject(new SpecificAIError('RISK_CONTROL', '人机验证已取消', { detail: { kind: 'captcha' } }));
|
|
126
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
127
|
+
const close = async () => {
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
130
|
+
server.closeAllConnections();
|
|
131
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
132
|
+
};
|
|
133
|
+
return { url: `http://127.0.0.1:${port}`, result, close };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* 在浏览器里手动过极验。只在终端里可用(stdin 与 stderr 都是 TTY),否则报 RISK_CONTROL。
|
|
137
|
+
* 地址直接写到 stderr(-q 也会显示,这一步必须由用户操作)。
|
|
138
|
+
*/
|
|
139
|
+
export async function manualGeetest(ctx, gt, challenge, options = {}) {
|
|
140
|
+
if (!interactive()) {
|
|
141
|
+
throw new SpecificAIError('RISK_CONTROL', '需要在浏览器里手动完成人机验证,但当前不是交互式终端', {
|
|
142
|
+
hint: '在终端里重新执行这条命令,按提示在浏览器里完成验证',
|
|
143
|
+
detail: { kind: 'captcha' },
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const s = await serveManual(gt, challenge, { ...options, signal: ctx.signal });
|
|
147
|
+
process.stderr.write(`[specific-ai] 请在浏览器打开 ${s.url} 完成人机验证(${options.timeout ?? TIMEOUT} 秒内),完成后自动继续\n`);
|
|
148
|
+
try {
|
|
149
|
+
return await s.result;
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
await s.close();
|
|
153
|
+
}
|
|
154
|
+
}
|