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,203 @@
|
|
|
1
|
+
import { SpecificAIError } from '../../../core/errors.js';
|
|
2
|
+
import { compactJson, pyFloatStr, urlencode } from '../../../core/py.js';
|
|
3
|
+
import * as rand from '../../../core/rand.js';
|
|
4
|
+
import { endpointFlag } from '../../../core/auth-store.js';
|
|
5
|
+
import { b3TraceId, Session, xrayTraceId } from './client.js';
|
|
6
|
+
import { checkSign, signFull } from './js.js';
|
|
7
|
+
import { CREATOR, CREATOR_BROWSER, CREATOR_ORDER, LOGIN_LANG, orderedHeaders, UA, XHR_ACCEPT } from './profile.js';
|
|
8
|
+
import { CreatorState, DS_REFRESH_MS } from './state.js';
|
|
9
|
+
/**
|
|
10
|
+
* 创作者中心的会话(上游 xhs_utils/xhs_creator:XHSCreatorAuth + CreatorDeviceProfile + params)。
|
|
11
|
+
* 凭证在 `creator` scope(AGENTS 5.3);签名状态存在凭证的 `device.creator`。
|
|
12
|
+
*/
|
|
13
|
+
const CREATOR_TIERS = { '0201': [200], '0101': [196, 197, 198] };
|
|
14
|
+
const DS_TTL = 300_000;
|
|
15
|
+
const AUTH_COOKIES = ['customer-sso-sid', 'access-token-creator.xiaohongshu.com', 'galaxy_creator_session_id', 'web_session'];
|
|
16
|
+
/** 发布页请求的 cookie 顺序(Chrome 152)。 */
|
|
17
|
+
export const PUBLISH_COOKIE_ORDER = [
|
|
18
|
+
'abRequestId', 'ets', 'a1', 'webId', 'gid', 'x-rednote-datactry', 'x-rednote-holderctry', '_gray_did', 'id_token',
|
|
19
|
+
'customer-sso-sid', 'x-user-id-creator.xiaohongshu.com', 'customerClientId', 'access-token-creator.xiaohongshu.com',
|
|
20
|
+
'galaxy_creator_session_id', 'galaxy.creator.beaker.session.id', 'web_session', 'unread', 'webBuild', 'xsecappid', 'acw_tc',
|
|
21
|
+
'websectiga', 'sec_poison_id', 'loadts',
|
|
22
|
+
];
|
|
23
|
+
/** Creator 的 splice_str:query 值里的 `:` 不转义。 */
|
|
24
|
+
export const cspl = (api, params) => `${api}?${urlencode(Object.entries(params).map(([k, v]) => [k, v ?? '']), { safe: ':' })}`;
|
|
25
|
+
/**
|
|
26
|
+
* Python 的 float。请求体由 Python 的 json.dumps 写出,整数值写成 `25.0`;签名 JS 拿到的是 JSON 解析后的对象,
|
|
27
|
+
* JSON.stringify 写成 `25`。toJSON 让签名侧照 JS 写,pyJson 让请求体照 Python 写。
|
|
28
|
+
*/
|
|
29
|
+
export class PyFloat {
|
|
30
|
+
value;
|
|
31
|
+
constructor(value) {
|
|
32
|
+
this.value = value;
|
|
33
|
+
}
|
|
34
|
+
toJSON() {
|
|
35
|
+
return this.value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** 与 compactJson 相同,PyFloat 按 Python 的 float 写。 */
|
|
39
|
+
export function pyJson(v) {
|
|
40
|
+
if (v instanceof PyFloat)
|
|
41
|
+
return pyFloatStr(v.value);
|
|
42
|
+
if (Array.isArray(v))
|
|
43
|
+
return `[${v.map(pyJson).join(',')}]`;
|
|
44
|
+
if (v && typeof v === 'object' && !(v instanceof Map))
|
|
45
|
+
return `{${Object.entries(v).map(([k, x]) => `${compactJson(k)}:${pyJson(x)}`).join(',')}}`;
|
|
46
|
+
return compactJson(v);
|
|
47
|
+
}
|
|
48
|
+
/** 上游 get_request_headers_template(Creator)。 */
|
|
49
|
+
function template(method, o) {
|
|
50
|
+
const h = {
|
|
51
|
+
'user-agent': UA,
|
|
52
|
+
accept: XHR_ACCEPT,
|
|
53
|
+
'accept-language': LOGIN_LANG,
|
|
54
|
+
'sec-fetch-dest': 'empty',
|
|
55
|
+
'sec-fetch-mode': 'cors',
|
|
56
|
+
'sec-fetch-site': o.site ?? 'same-site',
|
|
57
|
+
referer: o.referer ?? `${CREATOR}/`,
|
|
58
|
+
priority: 'u=1, i',
|
|
59
|
+
};
|
|
60
|
+
if (o.authorization ?? true)
|
|
61
|
+
h.authorization = '';
|
|
62
|
+
if (o.trace ?? true) {
|
|
63
|
+
h['x-b3-traceid'] = b3TraceId();
|
|
64
|
+
h['x-xray-traceid'] = xrayTraceId('core');
|
|
65
|
+
}
|
|
66
|
+
if (o.includeOrigin ?? method.toUpperCase() !== 'GET')
|
|
67
|
+
h.origin = o.origin ?? CREATOR;
|
|
68
|
+
if (method.toUpperCase() !== 'GET')
|
|
69
|
+
h['content-type'] = 'application/json';
|
|
70
|
+
if (o.clientHints) {
|
|
71
|
+
h['sec-ch-ua'] = '"Not;A=Brand";v="8", "Chromium";v="152", "Google Chrome";v="152"';
|
|
72
|
+
h['sec-ch-ua-mobile'] = '?0';
|
|
73
|
+
h['sec-ch-ua-platform'] = '"Windows"';
|
|
74
|
+
}
|
|
75
|
+
return h;
|
|
76
|
+
}
|
|
77
|
+
export class Creator extends Session {
|
|
78
|
+
state;
|
|
79
|
+
constructor(ctx) {
|
|
80
|
+
super(ctx, 'creator', CREATOR_BROWSER, CREATOR);
|
|
81
|
+
const device = (ctx.credential.device.creator ?? {});
|
|
82
|
+
this.state = new CreatorState(this.shared(), device.local);
|
|
83
|
+
}
|
|
84
|
+
resetState() {
|
|
85
|
+
this.state = new CreatorState(this.shared());
|
|
86
|
+
}
|
|
87
|
+
save() {
|
|
88
|
+
this.credential.device.creator = { local: this.state.storage() };
|
|
89
|
+
}
|
|
90
|
+
/** 缺 creator scope 时报 AUTH_REQUIRED,hint 带 --scope creator(AGENTS 5.3)。 */
|
|
91
|
+
requireScope() {
|
|
92
|
+
const c = this.state.cookies;
|
|
93
|
+
if (c.a1 && AUTH_COOKIES.some((k) => c[k]))
|
|
94
|
+
return;
|
|
95
|
+
const account = this.ctx.account && this.ctx.account !== 'guest' ? ` -a ${this.ctx.account}` : '';
|
|
96
|
+
throw new SpecificAIError('AUTH_REQUIRED', '需要登录小红书创作者中心', { hint: `specific-ai xhs auth login --scope creator${account}${endpointFlag(this.ctx.endpoint)}` });
|
|
97
|
+
}
|
|
98
|
+
/** 服务端 DS 程序与 _dsl(appId=ugc),5 分钟缓存(上游 xhs_creator/dsl.py)。 */
|
|
99
|
+
async dsBundle(force = false) {
|
|
100
|
+
const cached = this.credential.extra.creator_ds;
|
|
101
|
+
if (!force && cached?.dsl && cached.program && rand.now() - cached.at < DS_TTL)
|
|
102
|
+
return cached;
|
|
103
|
+
const { dsl, program } = await this.fetchDs('ugc', `${CREATOR}/`);
|
|
104
|
+
if (!dsl || !(program.includes('_dsf') || program.includes('__$c'))) {
|
|
105
|
+
if (cached?.dsl)
|
|
106
|
+
return cached;
|
|
107
|
+
throw new SpecificAIError('UPSTREAM', '小红书 ds 接口(ugc)没有返回 DS 程序,无法签名');
|
|
108
|
+
}
|
|
109
|
+
const bundle = { dsl, program, at: rand.now() };
|
|
110
|
+
this.credential.extra.creator_ds = bundle;
|
|
111
|
+
return bundle;
|
|
112
|
+
}
|
|
113
|
+
/** 上游 ensure_ds_material:安全就绪后保证 _dsl 与 _dsf 程序都在。 */
|
|
114
|
+
async ensureDs(force = false) {
|
|
115
|
+
if (!this.state.securityReady)
|
|
116
|
+
return;
|
|
117
|
+
const stale = rand.now() - this.state.dsllt >= DS_REFRESH_MS;
|
|
118
|
+
if (!force && !stale && this.state.dsl && this.state.dsProgram)
|
|
119
|
+
return;
|
|
120
|
+
const refresh = force || stale;
|
|
121
|
+
const b = await this.dsBundle(refresh);
|
|
122
|
+
if (refresh || !this.state.dsl)
|
|
123
|
+
this.state.dsl = b.dsl;
|
|
124
|
+
this.state.dsProgram = b.program;
|
|
125
|
+
if (refresh)
|
|
126
|
+
this.state.dsllt = rand.now();
|
|
127
|
+
}
|
|
128
|
+
/** 上游 generate_profile_request_params:签名 + 头模板,返回 headers 与紧凑 body。 */
|
|
129
|
+
async sign(api, data, method, o = {}, ensure = true) {
|
|
130
|
+
this.sync();
|
|
131
|
+
if (!this.state.cookies.a1)
|
|
132
|
+
throw new SpecificAIError('ERROR', '创作者中心 cookie 缺少 a1');
|
|
133
|
+
if (ensure && this.state.resolveMaterial(o.tier, o.mnsProfile).material.deviceTag !== 'nop')
|
|
134
|
+
await this.ensureDs();
|
|
135
|
+
const context = this.state.nextSignContext(o.tier, o.mnsProfile);
|
|
136
|
+
const b1 = o.b1Value ?? this.state.currentB1(context.now, o.b1Profile);
|
|
137
|
+
const dslPair = o.dslPairValue ?? this.state.dslPair(context.now);
|
|
138
|
+
if (context.tier === '0101' && context.deviceTag !== 'nop' && !context.dsProgram)
|
|
139
|
+
throw new SpecificAIError('ERROR', 'Creator mns0101 需要服务端下发的 DS 程序');
|
|
140
|
+
const input = { api, data: data ?? '', cookie: Object.entries(this.state.cookies).map(([k, v]) => `${k}=${v}`).join('; '), b1, dslPair, tier: context.tier };
|
|
141
|
+
for (const k of ['now', 'version', 'loadts', 'seq', 'envConst', 'envFpTail', 'deviceTag', 'b1b1', 'signCount', 'webBuild', 'signVersion', 'appId', 'platform', 'dsProgram', 'xt']) {
|
|
142
|
+
if (k in context)
|
|
143
|
+
input[k] = context[k];
|
|
144
|
+
}
|
|
145
|
+
if (context.dsProgram)
|
|
146
|
+
input.dsfProgram = context.dsProgram;
|
|
147
|
+
const r = checkSign(signFull(input), context.tier, CREATOR_TIERS[context.tier], 'Creator X-s');
|
|
148
|
+
if (!r.xs_common)
|
|
149
|
+
throw new SpecificAIError('ERROR', 'Creator X-S-Common 生成失败');
|
|
150
|
+
const headers = template(method, o);
|
|
151
|
+
headers['x-s'] = r.xs;
|
|
152
|
+
headers['x-t'] = String(r.xt);
|
|
153
|
+
headers['x-s-common'] = r.xs_common;
|
|
154
|
+
const body = data === '' || data == null ? '' : typeof data === 'string' ? data : pyJson(data);
|
|
155
|
+
return { headers, body };
|
|
156
|
+
}
|
|
157
|
+
/** 发往 creator.xiaohongshu.com 的 cookie 按发布页顺序重排。 */
|
|
158
|
+
wireFor(url) {
|
|
159
|
+
const cookies = this.wire(url);
|
|
160
|
+
if (!url.startsWith(CREATOR))
|
|
161
|
+
return cookies;
|
|
162
|
+
const out = {};
|
|
163
|
+
for (const k of PUBLISH_COOKIE_ORDER)
|
|
164
|
+
if (k in cookies)
|
|
165
|
+
out[k] = cookies[k];
|
|
166
|
+
for (const [k, v] of Object.entries(cookies))
|
|
167
|
+
if (!(k in out))
|
|
168
|
+
out[k] = v;
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 上游 XHS_Creator_Apis._request_params:b1 默认用 login 快照、XHR 不带 sec-ch-ua*,UA 固定 Chrome 152。
|
|
173
|
+
* order 为 null 时不排序(post_note 带 x-rap-param,上游没有核实过它的头顺序)。
|
|
174
|
+
*/
|
|
175
|
+
async business(api, data, method, o = {}) {
|
|
176
|
+
this.requireScope();
|
|
177
|
+
const signed = await this.sign(api, data, method, { b1Profile: 'login', origin: CREATOR, referer: `${CREATOR}/`, site: 'same-site', ...o, clientHints: false });
|
|
178
|
+
signed.headers['accept-language'] = LOGIN_LANG;
|
|
179
|
+
const url = (o.target ?? CREATOR) + api;
|
|
180
|
+
// 笔记管理页的请求再经 cookies_for_url 一次,回到存入顺序
|
|
181
|
+
const cookies = o.storeOrder ? this.wire(url) : this.wireFor(url);
|
|
182
|
+
let headers = { ...signed.headers, ...(o.extra ?? {}) };
|
|
183
|
+
headers['user-agent'] = UA;
|
|
184
|
+
let pairs;
|
|
185
|
+
if (o.order === null) {
|
|
186
|
+
// 不排序:dict 顺序,cookie 由 curl_cffi 的 cookies= 附上(accept-encoding 由调用方补在最后)
|
|
187
|
+
const cookie = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ');
|
|
188
|
+
pairs = Object.entries(headers).filter((p) => p[1] != null);
|
|
189
|
+
if (cookie)
|
|
190
|
+
pairs.push(['cookie', cookie]);
|
|
191
|
+
}
|
|
192
|
+
else
|
|
193
|
+
pairs = orderedHeaders(headers, o.order ?? (method === 'GET' ? CREATOR_ORDER.get : CREATOR_ORDER.post), cookies);
|
|
194
|
+
const ua = pairs.findIndex(([k]) => k === 'user-agent');
|
|
195
|
+
if (ua >= 0)
|
|
196
|
+
pairs[ua] = ['user-agent', UA];
|
|
197
|
+
return { url, headers: pairs, body: signed.body };
|
|
198
|
+
}
|
|
199
|
+
async request(api, data = '', method = 'GET', o = {}) {
|
|
200
|
+
const r = await this.business(api, data, method, o);
|
|
201
|
+
return this.json({ method, url: r.url, headers: r.headers, ...(method === 'POST' ? { body: r.body } : {}) });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import * as nodeCrypto from 'node:crypto';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import vm from 'node:vm';
|
|
7
|
+
import { SpecificAIError } from '../../../core/errors.js';
|
|
8
|
+
import { PACKAGE_ROOT, staticFile } from '../../../core/paths.js';
|
|
9
|
+
import * as rand from '../../../core/rand.js';
|
|
10
|
+
import { createContext } from '../../../core/vm.js';
|
|
11
|
+
/**
|
|
12
|
+
* 上游签名 JS(static/xhs/,原样复制,保持 xhs_core/js、xhs_pc/js、xhs_creator/js 的相对结构)。
|
|
13
|
+
*
|
|
14
|
+
* 上游按命令行脚本起 node 子进程(`node sign.js <json>`、`node b1.js --generate` 等),脚本本身都是 CommonJS 模块、
|
|
15
|
+
* 导出了真正干活的函数;这里在 node:vm 里按 CommonJS 加载它们,直接调用导出的函数。
|
|
16
|
+
* 确定性模式(对拍)下每次调用都新建 context——上游每次签名都新起一个进程,Math.random / Date 从头开始。
|
|
17
|
+
*
|
|
18
|
+
* websectiga_cli.js 例外:它改写进程级全局(隐藏 process、接管 Function.prototype.toString、
|
|
19
|
+
* `vm.runInThisContext` 跑服务端下发的程序),放不进 vm,和上游一样起子进程(AGENTS 7.4 的退路)。
|
|
20
|
+
*/
|
|
21
|
+
const hostRequire = createRequire(import.meta.url);
|
|
22
|
+
const P = 'xhs';
|
|
23
|
+
export const CORE_JS = (f) => staticFile(P, `xhs_core/js/${f}`);
|
|
24
|
+
export const PC_JS = (f) => staticFile(P, `xhs_pc/js/${f}`);
|
|
25
|
+
export const CREATOR_JS = (f) => staticFile(P, `xhs_creator/js/${f}`);
|
|
26
|
+
/**
|
|
27
|
+
* 脚本里的 `require('crypto')`:确定性模式下 randomBytes 改为从该 context 已固定的 Math.random 取值
|
|
28
|
+
* (每字节 floor(Math.random() * 256)),与对拍时预加载的 scripts/golden/xhs/crypto_determinism.cjs 相同。
|
|
29
|
+
*/
|
|
30
|
+
function newRealm() {
|
|
31
|
+
const context = createContext();
|
|
32
|
+
if (!rand.isDeterministic())
|
|
33
|
+
return { context, modules: new Map(), crypto: nodeCrypto };
|
|
34
|
+
const random = vm.runInContext('Math.random', context);
|
|
35
|
+
const randomBytes = (n) => Buffer.from(Array.from({ length: n }, () => Math.floor(random() * 256)));
|
|
36
|
+
const crypto = new Proxy(nodeCrypto, { get: (t, k) => (k === 'randomBytes' ? randomBytes : Reflect.get(t, k)) });
|
|
37
|
+
return { context, modules: new Map(), crypto };
|
|
38
|
+
}
|
|
39
|
+
let shared = null;
|
|
40
|
+
function realm() {
|
|
41
|
+
if (rand.isDeterministic())
|
|
42
|
+
return newRealm();
|
|
43
|
+
return (shared ??= newRealm());
|
|
44
|
+
}
|
|
45
|
+
function load(r, file) {
|
|
46
|
+
const cached = r.modules.get(file);
|
|
47
|
+
if (cached)
|
|
48
|
+
return cached.exports;
|
|
49
|
+
const module = { exports: {} };
|
|
50
|
+
r.modules.set(file, module);
|
|
51
|
+
if (file.endsWith('.json')) {
|
|
52
|
+
module.exports = JSON.parse(readFileSync(file, 'utf8'));
|
|
53
|
+
return module.exports;
|
|
54
|
+
}
|
|
55
|
+
const dir = dirname(file);
|
|
56
|
+
const req = (id) => {
|
|
57
|
+
if (id.startsWith('.')) {
|
|
58
|
+
const target = resolve(dir, id);
|
|
59
|
+
return load(r, /\.(js|json)$/.test(target) ? target : `${target}.js`);
|
|
60
|
+
}
|
|
61
|
+
return id === 'crypto' ? r.crypto : hostRequire(id);
|
|
62
|
+
};
|
|
63
|
+
const wrapper = vm.runInContext(`(function (exports, require, module, __filename, __dirname) {${readFileSync(file, 'utf8')}\n})`, r.context, {
|
|
64
|
+
filename: file,
|
|
65
|
+
});
|
|
66
|
+
wrapper(module.exports, req, module, file, dir);
|
|
67
|
+
return module.exports;
|
|
68
|
+
}
|
|
69
|
+
/** 加载一个模块(及其依赖),返回 module.exports。 */
|
|
70
|
+
export function jsModule(file) {
|
|
71
|
+
return load(realm(), file);
|
|
72
|
+
}
|
|
73
|
+
/** mns + X-s + X-t(+ X-S-Common,给了 dslPair 时)。入参与上游 runtime.run_signer 写给 node 的 JSON 相同。 */
|
|
74
|
+
export function signFull(input) {
|
|
75
|
+
return jsModule(CORE_JS('sign.js')).signFull(input);
|
|
76
|
+
}
|
|
77
|
+
/** 上游 run_signer 的输出门禁:mns 前缀与长度(PC)或长度集合(Creator)。 */
|
|
78
|
+
export function checkSign(result, tier, lengths, what = 'X-s') {
|
|
79
|
+
const x3 = String(result?.x3 ?? '');
|
|
80
|
+
if (!result?.xs || !x3.startsWith(`mns${tier}_`) || !lengths.includes(x3.length)) {
|
|
81
|
+
throw new SpecificAIError('ERROR', `小红书 ${what} 签名失败(mns${tier},长度 ${x3.length})`);
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
/** b1:19 个字段的环境快照 → RC4 → 自定义 base64(xhs_core/js/b1.js generateB1)。 */
|
|
86
|
+
export function generateB1(options) {
|
|
87
|
+
const value = String(jsModule(CORE_JS('b1.js')).generateB1(options).b1);
|
|
88
|
+
if (value.length < 500 || value.length % 4)
|
|
89
|
+
throw new SpecificAIError('ERROR', `小红书 b1 长度不对:${value.length}`);
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
/** PC webprofile 的 profileData(xhs_pc/js/profile.js)。 */
|
|
93
|
+
export function pcProfileData(options) {
|
|
94
|
+
return String(jsModule(PC_JS('profile.js')).generateProfileData(options));
|
|
95
|
+
}
|
|
96
|
+
/** Creator webprofile 的 profileData(xhs_creator/js/profile.js)。 */
|
|
97
|
+
export function creatorProfileData(options) {
|
|
98
|
+
return String(jsModule(CREATOR_JS('profile.js')).generateProfileData(options));
|
|
99
|
+
}
|
|
100
|
+
/** x-rap-param(xhs_pc/js/rap.js buildRapPure);fingerprintHex 为空时用 PC 的采集模板(与 rap_cli.js 相同)。 */
|
|
101
|
+
export function rapParam(api, data, fingerprintHex = '') {
|
|
102
|
+
const r = realm();
|
|
103
|
+
const fingerprint = Buffer.from(fingerprintHex || load(r, PC_JS('rap_fingerprint_template.json')).bodyUnmaskedHex, 'hex');
|
|
104
|
+
const value = String(load(r, PC_JS('rap.js')).buildRapPure({ api, data, fingerprint }));
|
|
105
|
+
if (!value.startsWith('ByQ'))
|
|
106
|
+
throw new SpecificAIError('ERROR', '小红书 x-rap-param 生成失败');
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
/** Creator 发布接口的 rap 指纹模板:03ea 段的 16 字符 Uuid 每次随机(上游 load_creator_rap_fingerprint_hex)。 */
|
|
110
|
+
export function creatorRapFingerprint() {
|
|
111
|
+
const raw = Buffer.from(jsModule(CREATOR_JS('rap_fingerprint_creator.json')).bodyUnmaskedHex, 'hex');
|
|
112
|
+
if (raw.subarray(0, 6).toString('hex') !== '03ea00000010')
|
|
113
|
+
throw new SpecificAIError('ERROR', 'Creator rap 指纹模板缺少 03ea 段');
|
|
114
|
+
Buffer.from(rand.string(16, '0123456789abcdefghijklmnopqrstuvwxyz'), 'ascii').copy(raw, 6);
|
|
115
|
+
return raw.toString('hex');
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 登录激活的 X25519 握手。上游用 generateKeyPairSync;这里私钥取 32 个随机字节(经 core/rand,对拍可固定),
|
|
119
|
+
* 公钥用 web_ssk.js 的 privateKeyFromRaw 导出——两者产出的密钥对等价。
|
|
120
|
+
*/
|
|
121
|
+
export function createHandshake() {
|
|
122
|
+
const priv = Buffer.from(rand.bytes(32));
|
|
123
|
+
const key = jsModule(PC_JS('web_ssk.js')).privateKeyFromRaw(priv);
|
|
124
|
+
const pub = nodeCrypto.createPublicKey(key).export({ format: 'der', type: 'spki' }).subarray(-32);
|
|
125
|
+
return { private_key_base64: priv.toString('base64'), client_public_key_base64: pub.toString('base64') };
|
|
126
|
+
}
|
|
127
|
+
/** 激活响应里的加密 SSK:X25519 + AES-256-GCM 解开,返回 base64。 */
|
|
128
|
+
export function acceptSsk(privateKeyBase64, encryptedSskBase64) {
|
|
129
|
+
return String(jsModule(PC_JS('web_ssk.js')).acceptSsk(privateKeyBase64, encryptedSskBase64));
|
|
130
|
+
}
|
|
131
|
+
// ---------------------------------------------------------------- Creator 上传(xhs_creator/js)
|
|
132
|
+
/** 文件加密接口的 sign:md5(固定盐 + fileId + 秒级时间戳的十六进制)。 */
|
|
133
|
+
export function urlSign(fileId) {
|
|
134
|
+
return String(jsModule(CREATOR_JS('xhs_creator_sign.js')).urlSing(fileId));
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* ROS 上传的 q-signature(xhs_creator_signature.js 的 getSignature)。
|
|
138
|
+
* 这个脚本是给 PyExecJS 用的:没有 module.exports,只有一个顶层函数,所以按普通脚本在 context 里执行后取出。
|
|
139
|
+
*/
|
|
140
|
+
export function uploadSignature(message, fileId, size, host) {
|
|
141
|
+
const r = realm();
|
|
142
|
+
const file = CREATOR_JS('xhs_creator_signature.js');
|
|
143
|
+
if (!r.modules.has(file)) {
|
|
144
|
+
vm.runInContext(readFileSync(file, 'utf8'), r.context, { filename: file });
|
|
145
|
+
r.modules.set(file, { exports: {} });
|
|
146
|
+
}
|
|
147
|
+
return String(r.context.getSignature(message, fileId, size, host));
|
|
148
|
+
}
|
|
149
|
+
// ---------------------------------------------------------------- websectiga(子进程)
|
|
150
|
+
/**
|
|
151
|
+
* 执行服务端下发的 seccallback 程序,得到 cookie `websectiga`(上游 runtime.generate_websectiga)。
|
|
152
|
+
* 确定性模式下和上游对拍时一样预加载 scripts/golden/node_determinism.cjs。
|
|
153
|
+
*/
|
|
154
|
+
export async function generateWebsectiga(code, profile) {
|
|
155
|
+
if (code.length < 1000)
|
|
156
|
+
throw new SpecificAIError('UPSTREAM', '小红书 seccallback 程序为空或被截断');
|
|
157
|
+
const deterministic = rand.isDeterministic();
|
|
158
|
+
const env = deterministic
|
|
159
|
+
? {
|
|
160
|
+
NODE_OPTIONS: `--require "${join(PACKAGE_ROOT, 'scripts', 'golden', 'node_determinism.cjs').replaceAll('\\', '/')}"`,
|
|
161
|
+
SPECIFIC_AI_GOLDEN_SEED: String(rand.DEFAULT_SEED),
|
|
162
|
+
SPECIFIC_AI_GOLDEN_NOW: String(rand.now()),
|
|
163
|
+
}
|
|
164
|
+
: { NODE_OPTIONS: '' };
|
|
165
|
+
const script = CORE_JS('websectiga_cli.js');
|
|
166
|
+
const input = JSON.stringify({ code, userAgent: profile.userAgent, platform: profile.platform, pageUrl: profile.pageUrl, ...(profile.timeoutMs ? { timeoutMs: profile.timeoutMs } : {}) });
|
|
167
|
+
const stdout = await new Promise((ok, fail) => {
|
|
168
|
+
const child = spawn(process.execPath, [script], { cwd: dirname(script), env: { ...process.env, ...env }, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
169
|
+
const out = [];
|
|
170
|
+
const timer = setTimeout(() => child.kill(), 30_000);
|
|
171
|
+
child.stdout.on('data', (d) => out.push(d));
|
|
172
|
+
child.stderr.on('data', () => { });
|
|
173
|
+
child.on('error', (e) => {
|
|
174
|
+
clearTimeout(timer);
|
|
175
|
+
fail(new SpecificAIError('ERROR', `websectiga 进程启动失败:${e.message}`));
|
|
176
|
+
});
|
|
177
|
+
child.on('close', () => {
|
|
178
|
+
clearTimeout(timer);
|
|
179
|
+
ok(Buffer.concat(out).toString('utf8'));
|
|
180
|
+
});
|
|
181
|
+
child.stdin.end(input);
|
|
182
|
+
});
|
|
183
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
184
|
+
if (!line.trim().startsWith('{'))
|
|
185
|
+
continue;
|
|
186
|
+
try {
|
|
187
|
+
const token = String(JSON.parse(line).websectiga ?? '');
|
|
188
|
+
if (/^[0-9a-f]{64}$/i.test(token))
|
|
189
|
+
return token;
|
|
190
|
+
}
|
|
191
|
+
catch { }
|
|
192
|
+
}
|
|
193
|
+
throw new SpecificAIError('RISK_CONTROL', '小红书安全程序执行失败,拿不到 websectiga', { detail: { kind: 'blocked' } });
|
|
194
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { crc32 } from 'node:zlib';
|
|
2
|
+
import { SpecificAIError } from '../../../core/errors.js';
|
|
3
|
+
import { md5Hex } from '../../../core/hash.js';
|
|
4
|
+
import { parseJson } from '../../../core/http.js';
|
|
5
|
+
import * as rand from '../../../core/rand.js';
|
|
6
|
+
import { checkStatus, getdss, jsonOf as parseResponse, loginOrder, splice } from './client.js';
|
|
7
|
+
import { acceptSsk, createHandshake, generateWebsectiga, pcProfileData } from './js.js';
|
|
8
|
+
import { AS, EDITH, LOGIN_LANG, navigationHeaders, orderedHeaders, PC_ORDER, PC_REFERENCE, SEM, WEB, XHR_ACCEPT } from './profile.js';
|
|
9
|
+
import { normalizeEts } from './state.js';
|
|
10
|
+
/**
|
|
11
|
+
* PC 端的匿名设备初始化与登录(上游 apis/xhs_pc_login_apis.py 的 XHSLoginApi)。
|
|
12
|
+
*
|
|
13
|
+
* 匿名链路:www 导航拿 abRequestId → 本地生成 a1 / webId → 安全初始化(honeypot、redcaptcha、sem_sdk、
|
|
14
|
+
* ds、sbtsource、seccallback → websectiga)→ login/activate 拿访客 web_session 与 webSsk → webprofile 拿 gid。
|
|
15
|
+
* 游客态就是这条链路走完的访客会话,缓存在 guest.json(AGENTS 5.2)。
|
|
16
|
+
*/
|
|
17
|
+
const A1_CHARSET = 'abcdefghijklmnopqrstuvwxyz1234567890';
|
|
18
|
+
/** 上游 common_util.generate_a1:时间戳十六进制 + 30 位随机 + '50000' + crc32,截到 52 位。 */
|
|
19
|
+
export function generateA1() {
|
|
20
|
+
const part = rand.now().toString(16) + rand.string(30, A1_CHARSET) + '5' + '0' + '000';
|
|
21
|
+
return (part + String(crc32(Buffer.from(part)) >>> 0)).slice(0, 52);
|
|
22
|
+
}
|
|
23
|
+
export const generateWebId = (a1) => md5Hex(a1);
|
|
24
|
+
/** 登录链路的一次请求:签名(可选)→ 按实抓顺序排头 → 发送 → 合并 cookie → 解析 JSON。 */
|
|
25
|
+
async function send(p, method, url, headers, kind, o = {}) {
|
|
26
|
+
const cookies = o.cookieUrl === null ? null : p.wire(o.cookieUrl ?? url);
|
|
27
|
+
const pairs = kind === 'raw' ? orderedHeaders(headers, PC_ORDER.navigation, cookies, { optional: ['cookie'] }) : orderedHeaders(headers, loginOrder(headers, kind), cookies);
|
|
28
|
+
return p.send({ method, url, headers: pairs, ...(o.body !== undefined ? { body: o.body } : {}) }, o.appendShared);
|
|
29
|
+
}
|
|
30
|
+
/** 登录接口触发风控时,退回到浏览器登录后导入 cookie。 */
|
|
31
|
+
const COOKIE_HINT = '在浏览器里登录小红书后导入 cookie:specific-ai xhs auth login --method cookie --cookie "<Cookie>"';
|
|
32
|
+
/** 登录链路的响应 → JSON:461 / 471 / 406 / 429 报风控并提示改用 cookie 登录,不是 JSON 时报 UPSTREAM。 */
|
|
33
|
+
const jsonOf = (res) => parseResponse(res, COOKIE_HINT);
|
|
34
|
+
// ---------------------------------------------------------------- 安全初始化
|
|
35
|
+
async function navigate(p) {
|
|
36
|
+
const h = navigationHeaders();
|
|
37
|
+
await p.send({ url: `${WEB}/`, headers: orderedHeaders(h, PC_ORDER.navigation, null, { optional: ['cookie'] }), redirect: 'manual' });
|
|
38
|
+
await p.send({ url: `${WEB}/explore`, headers: orderedHeaders(h, PC_ORDER.navigation, p.wire(`${WEB}/explore`), { optional: ['cookie'] }), redirect: 'manual' });
|
|
39
|
+
if (!p.shared().abRequestId)
|
|
40
|
+
throw new SpecificAIError('UPSTREAM', '小红书首页导航没有下发 abRequestId');
|
|
41
|
+
}
|
|
42
|
+
async function honeypot(p) {
|
|
43
|
+
const h = {
|
|
44
|
+
'sec-ch-ua-platform': '"Windows"',
|
|
45
|
+
referer: `${WEB}/`,
|
|
46
|
+
'user-agent': PC_REFERENCE.release.userAgent,
|
|
47
|
+
accept: XHR_ACCEPT,
|
|
48
|
+
'sec-ch-ua': PC_REFERENCE.release.secChUa,
|
|
49
|
+
'content-type': 'application/json;charset=UTF-8',
|
|
50
|
+
'sec-ch-ua-mobile': '?0',
|
|
51
|
+
'accept-language': LOGIN_LANG,
|
|
52
|
+
origin: WEB,
|
|
53
|
+
priority: 'u=1, i',
|
|
54
|
+
'sec-fetch-dest': 'empty',
|
|
55
|
+
'sec-fetch-mode': 'cors',
|
|
56
|
+
'sec-fetch-site': 'same-site',
|
|
57
|
+
};
|
|
58
|
+
await send(p, 'POST', `${AS}/api/p/pj`, h, 'honeypot', { body: '{"callFrom":"xhs-pc-web"}' });
|
|
59
|
+
}
|
|
60
|
+
async function security(p, api, payload, o) {
|
|
61
|
+
const s = await p.loginSigned(api, payload, 'POST', { secDomain: true, tier: o.tier, mnsProfile: o.mnsProfile, trace: o.trace ?? false, includeB1: o.includeB1 });
|
|
62
|
+
return send(p, 'POST', (o.origin ?? AS) + api, s.headers, o.kind ?? 'security', { body: s.body });
|
|
63
|
+
}
|
|
64
|
+
async function scripting(p, payload, tier, mnsProfile) {
|
|
65
|
+
const res = await security(p, '/api/sec/v1/scripting', payload, { tier, mnsProfile });
|
|
66
|
+
const body = await jsonOf(res);
|
|
67
|
+
if (!res.ok || !body?.success)
|
|
68
|
+
throw new SpecificAIError('UPSTREAM', body?.msg || `scripting HTTP ${res.status}`);
|
|
69
|
+
return body;
|
|
70
|
+
}
|
|
71
|
+
/** 上游 _initialize_security:冷启动两段安全程序,按浏览器顺序。 */
|
|
72
|
+
async function initializeSecurity(p) {
|
|
73
|
+
const dsl = await p.dsl();
|
|
74
|
+
p.setLoginDsl(dsl);
|
|
75
|
+
await honeypot(p);
|
|
76
|
+
await security(p, '/api/redcaptcha/v2/getconfig', {}, { tier: '0201', mnsProfile: 'security_initial', trace: true, origin: EDITH, kind: 'post' });
|
|
77
|
+
const sem = await p.loginSigned('/data/sem_sdk', '', 'GET', { secDomain: true, tier: '0201', mnsProfile: 'security_initial', trace: false });
|
|
78
|
+
await send(p, 'GET', `${SEM}/data/sem_sdk`, sem.headers, 'sem', { cookieUrl: null });
|
|
79
|
+
const ds = await scripting(p, { callFrom: 'web', callback: '', type: 'ds', appId: 'xhs-pc-web' }, '0201', 'security_initial');
|
|
80
|
+
const anchor = getdss(String(ds?.data?.data ?? ''));
|
|
81
|
+
if (!anchor)
|
|
82
|
+
throw new SpecificAIError('UPSTREAM', 'ds scripting 响应里没有 getdss()');
|
|
83
|
+
p.setLoginDsl(anchor);
|
|
84
|
+
await security(p, '/api/sec/v1/sbtsource', { callFrom: 'web', appId: 'xhs-pc-web' }, { tier: '0201', mnsProfile: 'security_initial' });
|
|
85
|
+
const sec = await scripting(p, { callFrom: 'web', callback: 'seccallback' }, '0101', 'security_callback');
|
|
86
|
+
const poison = String(sec?.data?.secPoisonId ?? '');
|
|
87
|
+
const code = String(sec?.data?.data ?? '');
|
|
88
|
+
if (!poison || code.length < 1000)
|
|
89
|
+
throw new SpecificAIError('UPSTREAM', 'seccallback 响应缺少 secPoisonId 或安全程序');
|
|
90
|
+
const tiga = await generateWebsectiga(code, { userAgent: PC_REFERENCE.release.userAgent, platform: 'Win32', pageUrl: `${WEB}/explore`, timeoutMs: 15000 });
|
|
91
|
+
p.setCookie('websectiga', tiga);
|
|
92
|
+
p.setCookie('sec_poison_id', poison);
|
|
93
|
+
p.sync();
|
|
94
|
+
p.state.lastTiga = rand.now();
|
|
95
|
+
p.state.dsllt = p.state.lastTiga;
|
|
96
|
+
}
|
|
97
|
+
/** 上游 _activate:访客 web_session + webSsk(X25519 协商)。 */
|
|
98
|
+
async function activate(p) {
|
|
99
|
+
const hs = createHandshake();
|
|
100
|
+
const s = await p.loginSigned('/api/sns/web/v1/login/activate', { client_public_key_base64: hs.client_public_key_base64 }, 'POST', { tier: '0101', mnsProfile: 'activate' });
|
|
101
|
+
const res = await send(p, 'POST', `${EDITH}/api/sns/web/v1/login/activate`, s.headers, 'post', { body: s.body });
|
|
102
|
+
const body = await jsonOf(res);
|
|
103
|
+
if (!res.ok || !body?.success)
|
|
104
|
+
throw new SpecificAIError('UPSTREAM', body?.msg || `login/activate HTTP ${res.status}`);
|
|
105
|
+
const d = body.data ?? {};
|
|
106
|
+
if (d.session)
|
|
107
|
+
p.setCookie('web_session', String(d.session));
|
|
108
|
+
p.sync();
|
|
109
|
+
if (!p.shared().web_session)
|
|
110
|
+
throw new SpecificAIError('UPSTREAM', 'login/activate 没有下发访客 web_session');
|
|
111
|
+
if (!d.ssk)
|
|
112
|
+
throw new SpecificAIError('UPSTREAM', 'login/activate 没有下发 SSK');
|
|
113
|
+
p.state.webSsk = JSON.stringify({ 'xhs-pc-web': acceptSsk(hs.private_key_base64, String(d.ssk)) });
|
|
114
|
+
p.state.fingerprintReady = true;
|
|
115
|
+
}
|
|
116
|
+
/** 上游 _fetch_gid / ensure_webprofile:上报 profileData,换 gid。 */
|
|
117
|
+
export async function webprofile(p) {
|
|
118
|
+
const existing = p.shared().gid;
|
|
119
|
+
if (p.webprofileReported && existing)
|
|
120
|
+
return existing;
|
|
121
|
+
const data = {
|
|
122
|
+
platform: 'Windows',
|
|
123
|
+
sdkVersion: PC_REFERENCE.release.webProfileSdkVersion,
|
|
124
|
+
svn: '2',
|
|
125
|
+
profileData: pcProfileData(p.state.profileDataOptions(rand.now())),
|
|
126
|
+
};
|
|
127
|
+
const res = await security(p, '/api/sec/v1/shield/webprofile', data, { tier: '0301', mnsProfile: 'webprofile', includeB1: true });
|
|
128
|
+
// 风控状态码照常抛出;只有响应体不是 JSON 时按上报失败处理(上游 except 后返回 None)
|
|
129
|
+
checkStatus(res, COOKIE_HINT);
|
|
130
|
+
const body = await parseJson(res).catch(() => ({}));
|
|
131
|
+
const gid = p.shared().gid;
|
|
132
|
+
if (!res.ok || !(body?.success === true || body?.code === 0) || !gid)
|
|
133
|
+
throw new SpecificAIError('RISK_CONTROL', 'webprofile 上报没有换到 gid', { detail: { kind: 'blocked' } });
|
|
134
|
+
p.state.p1 += 1;
|
|
135
|
+
p.state.fingerprintReady = true;
|
|
136
|
+
p.webprofileReported = true;
|
|
137
|
+
return gid;
|
|
138
|
+
}
|
|
139
|
+
/** 上游 generate_init_cookies:从零建立一个匿名设备会话(不含 webprofile)。 */
|
|
140
|
+
export async function initAnonymous(p) {
|
|
141
|
+
// 上游先用空 cookie 建一次 profile(消耗一次 tab 设备 ID 的 uuid4,这里是 Pc 构造时的那次),导航后再用初始 cookie 重建
|
|
142
|
+
p.jar.cookies.splice(0);
|
|
143
|
+
await navigate(p);
|
|
144
|
+
const ts = rand.now();
|
|
145
|
+
const a1 = generateA1();
|
|
146
|
+
const webId = generateWebId(a1);
|
|
147
|
+
const loadts = ts + rand.randint(50, 200);
|
|
148
|
+
p.replaceShared({
|
|
149
|
+
abRequestId: p.shared().abRequestId,
|
|
150
|
+
ets: String(normalizeEts(ts)),
|
|
151
|
+
webBuild: PC_REFERENCE.release.webBuild,
|
|
152
|
+
xsecappid: PC_REFERENCE.release.appId,
|
|
153
|
+
loadts: String(loadts),
|
|
154
|
+
a1,
|
|
155
|
+
webId,
|
|
156
|
+
});
|
|
157
|
+
p.resetState();
|
|
158
|
+
p.loginB1 = '';
|
|
159
|
+
p.webprofileReported = false;
|
|
160
|
+
await initializeSecurity(p);
|
|
161
|
+
await activate(p);
|
|
162
|
+
}
|
|
163
|
+
// ---------------------------------------------------------------- 二维码 / 短信
|
|
164
|
+
export async function qrcodeCreate(p) {
|
|
165
|
+
const s = await p.loginSigned('/api/sns/web/v1/login/qrcode/create', { qr_type: 1 }, 'POST', { tier: '0301', mnsProfile: 'qrcode_create' });
|
|
166
|
+
const body = await jsonOf(await send(p, 'POST', `${EDITH}/api/sns/web/v1/login/qrcode/create`, s.headers, 'post', { body: s.body }));
|
|
167
|
+
const d = body?.data ?? {};
|
|
168
|
+
if (!body?.success || !d.qr_id || !d.code || !d.url)
|
|
169
|
+
throw new SpecificAIError('UPSTREAM', body?.msg || '获取二维码失败');
|
|
170
|
+
return { qrId: String(d.qr_id), code: String(d.code), url: String(d.url) };
|
|
171
|
+
}
|
|
172
|
+
/** 上游 check_qrcode_status:0 待扫码、1 待确认、2 成功(成功时换正式 web_session)、3 过期。 */
|
|
173
|
+
export async function qrcodeStatus(p, qrId, code) {
|
|
174
|
+
const s = await p.loginSigned('/api/qrcode/userinfo', { qrId, code }, 'POST', { tier: '0301', mnsProfile: 'qrcode_poll' });
|
|
175
|
+
const body = await jsonOf(await send(p, 'POST', `${EDITH}/api/qrcode/userinfo`, s.headers, 'post', { body: s.body }));
|
|
176
|
+
const status = body?.data?.codeStatus;
|
|
177
|
+
if (status == null)
|
|
178
|
+
throw new SpecificAIError('UPSTREAM', body?.msg || '二维码状态响应缺少 codeStatus');
|
|
179
|
+
if (status === 2)
|
|
180
|
+
await qrcodeFinish(p, qrId, code);
|
|
181
|
+
return Number(status);
|
|
182
|
+
}
|
|
183
|
+
export async function qrcodeFinish(p, qrId, code) {
|
|
184
|
+
const api = splice('/api/sns/web/v1/login/qrcode/status', { qr_id: qrId, code });
|
|
185
|
+
const visitor = p.shared().web_session ?? '';
|
|
186
|
+
const s = await p.loginSigned(api, '', 'GET', { tier: '0301', mnsProfile: 'qrcode_poll' });
|
|
187
|
+
s.headers['x-login-mode'] = '';
|
|
188
|
+
const res = await send(p, 'GET', EDITH + api, s.headers, 'get-login-mode', { appendShared: ['web_session', 'id_token', 'x-rednote-datactry', 'x-rednote-holderctry'] });
|
|
189
|
+
const body = await jsonOf(res);
|
|
190
|
+
const d = body?.data ?? {};
|
|
191
|
+
if (!body?.success || d.code_status !== 2)
|
|
192
|
+
throw new SpecificAIError('AUTH_REQUIRED', body?.msg || '二维码登录状态无效');
|
|
193
|
+
// 上游 `cookies.pop(name); cookies[name] = value` 在 cookie dict 里挪到末尾,但 HostCookieStore 记住的发送顺序不变,
|
|
194
|
+
// 线上仍在原位,所以这里只改值
|
|
195
|
+
const session = String(d.login_info?.session ?? '');
|
|
196
|
+
if (session)
|
|
197
|
+
p.setCookie('web_session', session);
|
|
198
|
+
else if (!p.shared().web_session || p.shared().web_session === visitor)
|
|
199
|
+
throw new SpecificAIError('AUTH_REQUIRED', '二维码登录响应缺少正式 web_session');
|
|
200
|
+
p.sync();
|
|
201
|
+
}
|
|
202
|
+
export async function sendSmsCode(p, phone, zone = '86') {
|
|
203
|
+
const api = splice('/api/sns/web/v2/login/send_code', { phone, zone, type: 'login' });
|
|
204
|
+
const s = await p.loginSigned(api, '', 'GET');
|
|
205
|
+
const body = await jsonOf(await send(p, 'GET', EDITH + api, s.headers, 'get'));
|
|
206
|
+
if (!body?.success)
|
|
207
|
+
throw new SpecificAIError('UPSTREAM', body?.msg || '发送验证码失败', { detail: { code: body?.code } });
|
|
208
|
+
}
|
|
209
|
+
export async function smsLoginCode(p, phone, code, zone = '86') {
|
|
210
|
+
const api = splice('/api/sns/web/v1/login/check_code', { phone, zone, code });
|
|
211
|
+
const s = await p.loginSigned(api, '', 'GET');
|
|
212
|
+
const checked = await jsonOf(await send(p, 'GET', EDITH + api, s.headers, 'get'));
|
|
213
|
+
const token = checked?.data?.mobile_token;
|
|
214
|
+
if (!checked?.success || !token)
|
|
215
|
+
throw new SpecificAIError('AUTH_REQUIRED', checked?.msg || '验证码验证失败');
|
|
216
|
+
const login = '/api/sns/web/v2/login/code';
|
|
217
|
+
const s2 = await p.loginSigned(login, { mobile_token: token, zone, phone }, 'POST');
|
|
218
|
+
const body = await jsonOf(await send(p, 'POST', EDITH + login, s2.headers, 'post', { body: s2.body }));
|
|
219
|
+
const session = body?.data?.session;
|
|
220
|
+
if (!body?.success || !session)
|
|
221
|
+
throw new SpecificAIError('AUTH_REQUIRED', body?.msg || '登录失败');
|
|
222
|
+
p.setCookie('web_session', String(session));
|
|
223
|
+
p.sync();
|
|
224
|
+
}
|
|
225
|
+
/** 上游 XHSLoginApi.get_user_info:user/me,guest 为 false 才算正式登录。 */
|
|
226
|
+
export async function loginUserMe(p) {
|
|
227
|
+
const s = await p.loginSigned('/api/sns/web/v2/user/me', '', 'GET');
|
|
228
|
+
const body = await jsonOf(await send(p, 'GET', `${EDITH}/api/sns/web/v2/user/me`, s.headers, 'get'));
|
|
229
|
+
return body?.success ? (body.data ?? {}) : null;
|
|
230
|
+
}
|