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,694 @@
|
|
|
1
|
+
import { SpecificAIError } from '../../../core/errors.js';
|
|
2
|
+
import { compactJson, jsonLoads, quote } from '../../../core/py.js';
|
|
3
|
+
import * as rand from '../../../core/rand.js';
|
|
4
|
+
import { liveContext } from './client.js';
|
|
5
|
+
import * as gql from './gql.js';
|
|
6
|
+
import { genLikeToken } from './oracle.js';
|
|
7
|
+
import { ACCEPT_AXIOS, ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCEPT_LANGUAGE_SHORT, buildHeaders, CP, ID_HOST, LIVE, LIVE_SENTRY_BAGGAGE, PASSPORT, PRODUCT_LIVE, SEQUENCES, UA, WWW } from './profile.js';
|
|
8
|
+
import { mergeIssued, responseCookies } from './session.js';
|
|
9
|
+
/**
|
|
10
|
+
* 上游请求构造,一个函数对应一个上游方法,字段与顺序照抄(对拍测试逐字节比较):
|
|
11
|
+
* ks_apis/kuaishou_api.py(www)、login_api.py(passport / STS)、live_api.py(直播)、publish_api.py(创作者中心)、
|
|
12
|
+
* utils/ksuploader.py(分片上传)。
|
|
13
|
+
*/
|
|
14
|
+
// ================================================================ www:KuaishouAPI
|
|
15
|
+
/** get_feed_hot:首屏是 0 字节 POST,翻页发字面量 `{}`。 */
|
|
16
|
+
export function feedHot(ks, pcursor = '') {
|
|
17
|
+
return ks.wwwPost('/rest/v/feed/hot', pcursor ? {} : null);
|
|
18
|
+
}
|
|
19
|
+
/** get_video_detail:visionVideoDetail GraphQL。 */
|
|
20
|
+
export async function videoDetail(ks, photoId) {
|
|
21
|
+
const data = await ks.graphql('visionVideoDetail', { photoId, page: 'detail' }, gql.VIDEO_DETAIL, `${WWW}/short-video/${photoId}`, 'www_graphql_detail');
|
|
22
|
+
const detail = data?.data?.visionVideoDetail;
|
|
23
|
+
if (!detail || typeof detail !== 'object')
|
|
24
|
+
throw new SpecificAIError('UPSTREAM', 'visionVideoDetail 没有返回作品', { detail: { photoId } });
|
|
25
|
+
if (detail.status !== 1)
|
|
26
|
+
throw new SpecificAIError('UPSTREAM', `作品不可见或不存在(status=${detail.status})`, { detail: { photoId, status: detail.status } });
|
|
27
|
+
return detail;
|
|
28
|
+
}
|
|
29
|
+
/** get_short_video_reco:详情页旁路的推荐流。 */
|
|
30
|
+
export async function shortVideoReco(ks, photoId) {
|
|
31
|
+
const data = await ks.graphql('visionShortVideoReco', { page: 'detail', photoId }, gql.SHORT_VIDEO_RECO, `${WWW}/short-video/${photoId}`, 'www_graphql_detail');
|
|
32
|
+
return data?.data?.visionShortVideoReco ?? {};
|
|
33
|
+
}
|
|
34
|
+
/** get_comment_list:REST 一级评论。 */
|
|
35
|
+
export function commentList(ks, photoId, pcursor = '') {
|
|
36
|
+
return ks.wwwPost('/rest/v/photo/comment/list', { photoId, pcursor });
|
|
37
|
+
}
|
|
38
|
+
/** get_comment_list_gql:作品详情页用的 GraphQL 评论(数据在 V2 字段里)。 */
|
|
39
|
+
export async function commentListGql(ks, photoId, pcursor = '') {
|
|
40
|
+
const data = await ks.graphql('commentListQuery', { photoId, pcursor }, gql.COMMENT_LIST, `${WWW}/short-video/${photoId}`, 'www_graphql_detail');
|
|
41
|
+
return data?.data?.visionCommentList ?? {};
|
|
42
|
+
}
|
|
43
|
+
/** get_sub_comment_list:“查看更多回复”。 */
|
|
44
|
+
export async function subCommentList(ks, photoId, rootCommentId, pcursor = '') {
|
|
45
|
+
const data = await ks.graphql('visionSubCommentList', { photoId, rootCommentId, pcursor }, gql.SUB_COMMENT_LIST, `${WWW}/short-video/${photoId}`, 'www_graphql_detail');
|
|
46
|
+
return data?.data?.visionSubCommentList ?? {};
|
|
47
|
+
}
|
|
48
|
+
/** get_profile:当前登录用户资料,顺带缓存本人 eid。 */
|
|
49
|
+
export async function profile(ks, referer) {
|
|
50
|
+
const result = await ks.wwwGet('/rest/v/profile/get', { referer });
|
|
51
|
+
const eid = String(result?.eid ?? result?.userDefineId ?? '');
|
|
52
|
+
if (eid)
|
|
53
|
+
ks.s.selfEid = eid;
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
/** get_profile_feed:指定用户的作品,body 键名是 user_id;请求头多一个空的 profile_referer。 */
|
|
57
|
+
export function profileFeed(ks, eid, pcursor = '') {
|
|
58
|
+
return ks.wwwPost('/rest/v/profile/feed', { user_id: eid, pcursor, page: 'profile' }, { referer: `https://www.kuaishou.com/profile/${eid}`, extraHeaders: [['profile_referer', '']] });
|
|
59
|
+
}
|
|
60
|
+
const searchReferer = (keyword) => `https://www.kuaishou.com/search/video?searchKey=${quote(keyword, '')}`;
|
|
61
|
+
/** search_feed:首屏带 page / webPageArea,翻页带首屏返回的 searchSessionId。 */
|
|
62
|
+
export function searchFeed(ks, keyword, pcursor = '', sessionId = '') {
|
|
63
|
+
const body = pcursor ? { keyword, pcursor, searchSessionId: sessionId } : { keyword, page: 'search', webPageArea: '', pcursor: '' };
|
|
64
|
+
return ks.wwwPost('/rest/v/search/feed', body, { referer: searchReferer(keyword) });
|
|
65
|
+
}
|
|
66
|
+
/** search_user。 */
|
|
67
|
+
export function searchUser(ks, keyword, pcursor = '', sessionId = '') {
|
|
68
|
+
return ks.wwwPost('/rest/v/search/user', { keyword, pcursor, searchSessionId: sessionId }, { referer: searchReferer(keyword) });
|
|
69
|
+
}
|
|
70
|
+
/** get_relation:关注(ftype=1)/ 粉丝(2),只有本人;Referer 是本人主页。 */
|
|
71
|
+
export function relation(ks, ftype, pcursor = '') {
|
|
72
|
+
return ks.wwwPost('/rest/v/relation/fol', { pcursor, ftype }, { referer: `${WWW}/profile/${ks.s.selfEid}`, cookieProfile: ftype === 1 ? 'www_relogin_relation_following' : 'www_relogin_relation_fans' });
|
|
73
|
+
}
|
|
74
|
+
/** get_liked_list:我的喜欢。 */
|
|
75
|
+
export function likedList(ks, pcursor = '', referer) {
|
|
76
|
+
return ks.wwwPost('/rest/v/feed/liked', { pcursor, page: 'profile' }, { referer });
|
|
77
|
+
}
|
|
78
|
+
/** get_collect_list:收藏,body 里是 eid(驼峰 userId)。 */
|
|
79
|
+
export function collectList(ks, eid, pcursor = '') {
|
|
80
|
+
return ks.wwwPost('/rest/v/collect/list', { userId: eid, pcursor, page: 'collect' }, { referer: eid ? `https://www.kuaishou.com/profile/${eid}` : undefined });
|
|
81
|
+
}
|
|
82
|
+
/** get_playback_list:用户的直播回放。 */
|
|
83
|
+
export function playbackList(ks, eid, pcursor = '') {
|
|
84
|
+
return ks.wwwPost('/rest/v/live/playBack/list', { userId: eid, pcursor }, { referer: `https://www.kuaishou.com/profile/${eid}` });
|
|
85
|
+
}
|
|
86
|
+
const MY_FOLLOW = 'https://www.kuaishou.com/myFollow';
|
|
87
|
+
/** like_data:关注页的 likeDataQuery,token 由抓包 bundle 的引擎按 {did, ts, uri} 生成。 */
|
|
88
|
+
export async function likeData(ks, page = 'follow') {
|
|
89
|
+
const st = rand.now();
|
|
90
|
+
const token = genLikeToken(ks.s.did, st, '/rest/v/feed/myfollow');
|
|
91
|
+
const data = await ks.graphql('likeDataQuery', { token, st, page }, gql.LIKE_DATA, MY_FOLLOW);
|
|
92
|
+
return data?.data?.likeData ?? {};
|
|
93
|
+
}
|
|
94
|
+
/** vision_profile_reduced:用户资料(不带 userId 时是当前用户)。 */
|
|
95
|
+
export async function profileReduced(ks, userId) {
|
|
96
|
+
const data = await ks.graphql('visionProfileReduced', userId == null ? {} : { userId }, gql.VISION_PROFILE_REDUCED, MY_FOLLOW);
|
|
97
|
+
return data?.data?.visionProfileReduced ?? {};
|
|
98
|
+
}
|
|
99
|
+
/** get_user_info:userInfoQuery,登录用户的轻量信息(直接给 eid)。 */
|
|
100
|
+
export async function userInfo(ks) {
|
|
101
|
+
const data = await ks.graphql('userInfoQuery', {}, gql.USER_INFO, MY_FOLLOW);
|
|
102
|
+
return data?.data?.userInfo ?? {};
|
|
103
|
+
}
|
|
104
|
+
// ================================================================ 登录:KuaishouLoginAPI
|
|
105
|
+
export const SID_LIVE = 'kuaishou.live.web';
|
|
106
|
+
export const SID_WEB_API = 'kuaishou.web.api';
|
|
107
|
+
export const SID_WWW = 'kuaishou.server.webday7';
|
|
108
|
+
export const SID_CP = 'kuaishou.web.cp.api';
|
|
109
|
+
const CHANNEL_PC = 'PC_PAGE';
|
|
110
|
+
const CHANNEL_UNKNOWN = 'UNKNOWN';
|
|
111
|
+
const SITE_BY_SID = { [SID_WWW]: WWW, [SID_CP]: CP, [SID_LIVE]: LIVE };
|
|
112
|
+
const SESSION_KEYS = ['passToken', 'userId', 'ssecurity', 'bUserId'];
|
|
113
|
+
export const channelFor = (sid) => (sid === SID_WWW ? CHANNEL_UNKNOWN : CHANNEL_PC);
|
|
114
|
+
/** 扫码流程实抓的页面来源:www sid 在 www 页面,直播 / 手机号在 passport 页面。 */
|
|
115
|
+
function qrPageOrigin(sid, channel) {
|
|
116
|
+
if (sid === SID_WWW || (!sid && channel === CHANNEL_UNKNOWN))
|
|
117
|
+
return WWW;
|
|
118
|
+
return PASSPORT;
|
|
119
|
+
}
|
|
120
|
+
/** 登录响应的票据:Set-Cookie,加上正文里的 passToken / userId / *_st / *.at。 */
|
|
121
|
+
function sessionCookies(data, res) {
|
|
122
|
+
const cookies = responseCookies(res);
|
|
123
|
+
for (const key of SESSION_KEYS)
|
|
124
|
+
if (data?.[key])
|
|
125
|
+
mergeIssued(cookies, [[key, String(data[key])]]);
|
|
126
|
+
for (const [key, value] of Object.entries(data ?? {})) {
|
|
127
|
+
if (typeof value === 'string' && value && (key.endsWith('_st') || key.endsWith('.at')))
|
|
128
|
+
mergeIssued(cookies, [[key, value]]);
|
|
129
|
+
}
|
|
130
|
+
return cookies;
|
|
131
|
+
}
|
|
132
|
+
export async function qrStart(ks, sid, channel = channelFor(sid)) {
|
|
133
|
+
const [data] = await ks.loginForm('/rest/c/infra/ks/qr/start', [
|
|
134
|
+
['sid', sid],
|
|
135
|
+
['channelType', channel],
|
|
136
|
+
['isWebSig4', 'true'],
|
|
137
|
+
], qrPageOrigin(sid, channel));
|
|
138
|
+
return data;
|
|
139
|
+
}
|
|
140
|
+
export async function qrScanResult(ks, token, signature, channel) {
|
|
141
|
+
const [data] = await ks.loginForm('/rest/c/infra/ks/qr/scanResult', [
|
|
142
|
+
['qrLoginToken', token],
|
|
143
|
+
['qrLoginSignature', signature],
|
|
144
|
+
['channelType', channel],
|
|
145
|
+
['isWebSig4', 'true'],
|
|
146
|
+
], qrPageOrigin('', channel), 70);
|
|
147
|
+
return data;
|
|
148
|
+
}
|
|
149
|
+
export async function qrAcceptResult(ks, token, signature, sid, channel) {
|
|
150
|
+
const [data] = await ks.loginForm('/rest/c/infra/ks/qr/acceptResult', [
|
|
151
|
+
['qrLoginToken', token],
|
|
152
|
+
['qrLoginSignature', signature],
|
|
153
|
+
['sid', sid],
|
|
154
|
+
['channelType', channel],
|
|
155
|
+
['isWebSig4', 'true'],
|
|
156
|
+
], qrPageOrigin(sid, channel), 70);
|
|
157
|
+
return data;
|
|
158
|
+
}
|
|
159
|
+
/** qr_callback:用 qrToken 换登录态,票据在正文和 Set-Cookie 里。 */
|
|
160
|
+
export async function qrCallback(ks, qrToken, sid, channel) {
|
|
161
|
+
const [data, res] = await ks.loginForm('/pass/kuaishou/login/qr/callback', [
|
|
162
|
+
['qrToken', qrToken],
|
|
163
|
+
['sid', sid],
|
|
164
|
+
['channelType', channel],
|
|
165
|
+
['isWebSig4', 'true'],
|
|
166
|
+
], qrPageOrigin(sid, channel));
|
|
167
|
+
return [data, sessionCookies(data, res)];
|
|
168
|
+
}
|
|
169
|
+
/** request_mobile_code:手机号登录短信(type=53 即 LOGIN)。 */
|
|
170
|
+
export async function requestMobileCode(ks, phone, countryCode = '+86') {
|
|
171
|
+
const [data] = await ks.loginForm('/pass/kuaishou/sms/requestMobileCode', [
|
|
172
|
+
['channelType', CHANNEL_PC],
|
|
173
|
+
['countryCode', countryCode],
|
|
174
|
+
['isWebSig4', 'true'],
|
|
175
|
+
['phone', phone],
|
|
176
|
+
['sid', SID_WEB_API],
|
|
177
|
+
['type', '53'],
|
|
178
|
+
], PASSPORT, 30);
|
|
179
|
+
return data;
|
|
180
|
+
}
|
|
181
|
+
/** login_by_mobile_code 的登录请求本身。 */
|
|
182
|
+
export async function mobileCodeLogin(ks, phone, smsCode, countryCode = '+86') {
|
|
183
|
+
const [data, res] = await ks.loginForm('/pass/kuaishou/login/mobileCode', [
|
|
184
|
+
['channelType', CHANNEL_PC],
|
|
185
|
+
['countryCode', countryCode],
|
|
186
|
+
['createId', 'true'],
|
|
187
|
+
['isWebSig4', 'true'],
|
|
188
|
+
['phone', phone],
|
|
189
|
+
['setCookie', 'true'],
|
|
190
|
+
['sid', SID_WEB_API],
|
|
191
|
+
['smsCode', smsCode],
|
|
192
|
+
], PASSPORT);
|
|
193
|
+
return [data, sessionCookies(data, res)];
|
|
194
|
+
}
|
|
195
|
+
/** STS 两跳是 iframe 导航,不是 XHR。 */
|
|
196
|
+
function navHeaders(referer) {
|
|
197
|
+
return [
|
|
198
|
+
['user-agent', UA],
|
|
199
|
+
['upgrade-insecure-requests', '1'],
|
|
200
|
+
['accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7'],
|
|
201
|
+
['sec-fetch-site', 'same-site'],
|
|
202
|
+
['sec-fetch-mode', 'navigate'],
|
|
203
|
+
['sec-fetch-dest', 'iframe'],
|
|
204
|
+
['referer', referer],
|
|
205
|
+
['accept-encoding', ACCEPT_ENCODING],
|
|
206
|
+
['accept-language', ACCEPT_LANGUAGE_SHORT],
|
|
207
|
+
];
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* sts_login:用 passToken 换站点域名下的 `<sid>_st` / `<sid>_ph`(两跳 302,第二跳的 authToken 由服务端补上)。
|
|
211
|
+
*/
|
|
212
|
+
export async function stsLogin(ks, sid, site = SITE_BY_SID[sid]) {
|
|
213
|
+
ks.s.useSite(site);
|
|
214
|
+
const ssoId = `SSO_${rand.now()}`;
|
|
215
|
+
const result = (ok) => `${PASSPORT}/pc/account/passToken/result?successful=${ok}&id=${ssoId}&for=passTokenSuccess`;
|
|
216
|
+
const stsUrl = `${site}/rest/infra/sts?followUrl=${quote(result('true'), '')}&failUrl=${quote(result('false'), '')}&setRootDomain=${sid === SID_CP ? 'true' : 'false'}`;
|
|
217
|
+
const loginPage = `${PASSPORT}/pc/account/passToken/result?successful=false&id=${ssoId}&for=pullTokenFail`;
|
|
218
|
+
const url = `${ID_HOST}/pass/kuaishou/login/passToken?callback=${quote(stsUrl, '')}&__loginPage=${quote(loginPage, '')}&sid=${quote(sid, '')}`;
|
|
219
|
+
const headers = navHeaders(`${site}/`);
|
|
220
|
+
const first = await ks.send({ method: 'GET', url, headers, cookie: await ks.s.currentLine(), redirect: 'manual' });
|
|
221
|
+
const issued = responseCookies(first);
|
|
222
|
+
const location = first.headers.get('location');
|
|
223
|
+
if (!location) {
|
|
224
|
+
ks.s.update(issued);
|
|
225
|
+
return issued;
|
|
226
|
+
}
|
|
227
|
+
const second = await ks.send({ method: 'GET', url: location, headers, cookie: await ks.s.currentLine(), redirect: 'manual' });
|
|
228
|
+
mergeIssued(issued, responseCookies(second));
|
|
229
|
+
ks.s.update(issued);
|
|
230
|
+
return issued;
|
|
231
|
+
}
|
|
232
|
+
/** bootstrap_document:加载站点文档,接收页面级 Set-Cookie(如 ktrace-context)。 */
|
|
233
|
+
export async function bootstrapDocument(ks, site, path) {
|
|
234
|
+
const h = buildHeaders('DOC');
|
|
235
|
+
h.set('sec-fetch-site', 'none');
|
|
236
|
+
const res = await ks.send({ method: 'GET', url: `${site}${path}`, headers: h.get(), cookie: await ks.s.currentLine() });
|
|
237
|
+
const issued = responseCookies(res);
|
|
238
|
+
ks.s.update(issued);
|
|
239
|
+
return issued;
|
|
240
|
+
}
|
|
241
|
+
/** 直播页换票时的 Cookie 线序:没有创作者中心票据时是 `_direct` 那一套。 */
|
|
242
|
+
function passTokenProfile(ks, referer) {
|
|
243
|
+
const base = liveContext(referer) === 'room' ? 'live_pass_token_room' : 'live_pass_token_home';
|
|
244
|
+
return ks.s.cookies.get('kuaishou.web.cp.api_st') ? base : `${base}_direct`;
|
|
245
|
+
}
|
|
246
|
+
/** get_cdns:passToken 之前的预检,也是一次直播页导航的开始(新的 Sentry trace)。 */
|
|
247
|
+
export async function getCdns(ks, referer) {
|
|
248
|
+
ks.s.beginLiveTransaction(referer);
|
|
249
|
+
const cookie = await ks.s.cookieHeader(passTokenProfile(ks, referer));
|
|
250
|
+
const h = buildHeaders('POST', 'login_pass_token');
|
|
251
|
+
h.set('content-type', 'application/json');
|
|
252
|
+
h.set('referer', referer);
|
|
253
|
+
h.set('origin', LIVE);
|
|
254
|
+
h.set('sec-fetch-site', 'same-site');
|
|
255
|
+
const res = await ks.send({ method: 'POST', url: `${ID_HOST}/pass/kuaishou/getCdns`, headers: h.get(), cookie, body: compactJson({ sid: SID_LIVE, did: ks.s.did }) });
|
|
256
|
+
return res.json().catch(() => ({}));
|
|
257
|
+
}
|
|
258
|
+
/** pass_token_login:用 passToken 给直播站铸一份新鲜的 `<sid>_st` / `<sid>.at`。 */
|
|
259
|
+
export async function passTokenLogin(ks, referer = `${LIVE}/`) {
|
|
260
|
+
await getCdns(ks, referer);
|
|
261
|
+
const h = buildHeaders('POST', 'login_pass_token');
|
|
262
|
+
h.set('referer', referer);
|
|
263
|
+
h.set('origin', LIVE);
|
|
264
|
+
h.set('sec-fetch-site', 'same-site');
|
|
265
|
+
const cookie = await ks.s.cookieHeader(passTokenProfile(ks, referer));
|
|
266
|
+
const body = `sid=${quote(SID_LIVE, '')}&channelType=${quote(CHANNEL_UNKNOWN, '')}&encryptHeaders=`;
|
|
267
|
+
const res = await ks.send({ method: 'POST', url: `${ID_HOST}/pass/kuaishou/login/passToken`, headers: h.get(), cookie, body });
|
|
268
|
+
const data = await res
|
|
269
|
+
.clone()
|
|
270
|
+
.json()
|
|
271
|
+
.catch(() => ({}));
|
|
272
|
+
return [data, sessionCookies(data, res)];
|
|
273
|
+
}
|
|
274
|
+
/** user_login_session:直播站 userLogin(authToken),真正可用的 live web_st / web_ph 在 Set-Cookie 里。 */
|
|
275
|
+
export async function userLoginSession(ks, authToken, referer = `${LIVE}/`) {
|
|
276
|
+
const context = liveContext(referer);
|
|
277
|
+
const hasPh = Boolean(ks.s.cookies.get('kuaishou.live.web_ph'));
|
|
278
|
+
let profile = context === 'room' ? ks.roomProfile('login') : context === 'home' ? ks.homeProfile('login') : `live_${context}_login`;
|
|
279
|
+
if (context === 'room' && !hasPh)
|
|
280
|
+
profile = ks.s.liveOnly ? 'live_room_current_login_bootstrap' : 'live_room_login_bootstrap';
|
|
281
|
+
if (context === 'home' && !hasPh)
|
|
282
|
+
profile = ks.s.liveOnly ? ks.homeProfile('login_bootstrap') : 'live_home_login_bootstrap';
|
|
283
|
+
const [data, res] = await ks.live('POST', '/live_api/baseuser/userLogin', {
|
|
284
|
+
body: { userLoginInfo: { authToken, sid: SID_LIVE } },
|
|
285
|
+
referer,
|
|
286
|
+
sentry: true,
|
|
287
|
+
cookieProfile: profile,
|
|
288
|
+
});
|
|
289
|
+
const ok = data?.data?.result === 1;
|
|
290
|
+
if (ok) {
|
|
291
|
+
if (context === 'room' && ks.s.liveOnly)
|
|
292
|
+
ks.s.advanceLive(context, 'authenticated');
|
|
293
|
+
else
|
|
294
|
+
ks.s.advanceLive(context, context === 'home' ? 'authenticated_1' : 'login');
|
|
295
|
+
}
|
|
296
|
+
return [ok, responseCookies(res)];
|
|
297
|
+
}
|
|
298
|
+
/** refresh_site_session:passToken 换票 + userLogin,重建直播站会话。 */
|
|
299
|
+
export async function refreshLiveSession(ks, referer = `${LIVE}/`) {
|
|
300
|
+
const [data, cookies] = await passTokenLogin(ks, referer);
|
|
301
|
+
if (data?.result !== 1)
|
|
302
|
+
return false;
|
|
303
|
+
ks.s.update(cookies);
|
|
304
|
+
const token = data?.[`${SID_LIVE}.at`] ?? cookies.find(([k]) => k === `${SID_LIVE}.at`)?.[1] ?? '';
|
|
305
|
+
if (!token)
|
|
306
|
+
return false;
|
|
307
|
+
const [ok, issued] = await userLoginSession(ks, token, referer);
|
|
308
|
+
if (!ok)
|
|
309
|
+
return false;
|
|
310
|
+
ks.s.update(issued);
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
// ================================================================ 直播:KuaishouLiveAPI
|
|
314
|
+
export const roomReferer = (eid) => `${LIVE}/u/${eid}`;
|
|
315
|
+
export async function homeList(ks) {
|
|
316
|
+
const [data] = await ks.live('GET', '/live_api/home/list', { referer: `${LIVE}/`, cookieProfile: ks.homeProfile('initial') });
|
|
317
|
+
return data;
|
|
318
|
+
}
|
|
319
|
+
export async function categoryClassify(ks, referer = `${LIVE}/`) {
|
|
320
|
+
const [data] = await ks.live('GET', '/live_api/category/classify', {
|
|
321
|
+
query: [
|
|
322
|
+
['type', 4],
|
|
323
|
+
['source', 2],
|
|
324
|
+
['page', 1],
|
|
325
|
+
['pageSize', 20],
|
|
326
|
+
],
|
|
327
|
+
referer,
|
|
328
|
+
});
|
|
329
|
+
return data;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* gift_list:首屏(只有 liveStreamId,带 Sentry,当前阶段的 Cookie 线序);
|
|
333
|
+
* 传 sortType 时是点击“更多礼物”(`liveStreamId&sortType=0`,不带 Sentry,authenticated 线序)。
|
|
334
|
+
*/
|
|
335
|
+
export async function giftList(ks, liveStreamId, eid, sortType) {
|
|
336
|
+
const more = sortType != null;
|
|
337
|
+
const query = [['liveStreamId', liveStreamId]];
|
|
338
|
+
if (more)
|
|
339
|
+
query.push(['sortType', sortType]);
|
|
340
|
+
const [data] = await ks.live('GET', '/live_api/emoji/gift-list', {
|
|
341
|
+
query,
|
|
342
|
+
referer: roomReferer(eid),
|
|
343
|
+
sentry: !more,
|
|
344
|
+
cookieProfile: more ? ks.roomProfile('authenticated') : undefined,
|
|
345
|
+
});
|
|
346
|
+
return data;
|
|
347
|
+
}
|
|
348
|
+
/** emoji_all_gifts:房间首屏的完整礼物字典(`/emoji/allgifts`,带 Sentry,房间资源的 Cookie 线序)。 */
|
|
349
|
+
export async function emojiAllGifts(ks, eid) {
|
|
350
|
+
const [data] = await ks.live('GET', '/live_api/emoji/allgifts', { referer: roomReferer(eid), sentry: true, cookieProfile: 'live_room_assets_initial' });
|
|
351
|
+
return data;
|
|
352
|
+
}
|
|
353
|
+
/** 登出只放行上游抓包保留的那个房间页 Referer(上游拒绝外推到别的页面)。 */
|
|
354
|
+
export const LIVE_LOGOUT_EID = '3xtgiecv6inq83i';
|
|
355
|
+
const LIVE_LOGOUT_REFERER = roomReferer(LIVE_LOGOUT_EID);
|
|
356
|
+
const PASSPORT_LOGOUT_BODY = 'sid=kuaishou.live.web&channelType=UNKNOWN&encryptHeaders=';
|
|
357
|
+
const LOGOUT_SET_COOKIE = { passport: ['userId', 'userId', 'passToken'], live: ['kuaishou.live.web_st', 'kuaishou.live.web_ph', 'userId'] };
|
|
358
|
+
const LOGOUT_CONTENT_TYPE = { passport: 'application/json;charset=utf-8', live: 'application/json; charset=utf-8' };
|
|
359
|
+
/** 登出响应:状态码、content-type、Set-Cookie 删除的名字与顺序;已清理过的会话还要求响应体一致。 */
|
|
360
|
+
async function checkLogout(res, kind, requireBody) {
|
|
361
|
+
const text = await res.text();
|
|
362
|
+
let payload;
|
|
363
|
+
try {
|
|
364
|
+
payload = jsonLoads(text);
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
payload = { result: -1, error: text.slice(0, 500) };
|
|
368
|
+
}
|
|
369
|
+
const fail = (why) => new SpecificAIError('UPSTREAM', `快手登出(${kind})${why}`, { detail: { status: res.status, payload } });
|
|
370
|
+
if (res.status !== 200)
|
|
371
|
+
throw fail(`HTTP 状态不是 200:${res.status}`);
|
|
372
|
+
if ((res.headers.get('content-type') ?? '').toLowerCase() !== LOGOUT_CONTENT_TYPE[kind])
|
|
373
|
+
throw fail('content-type 与浏览器不一致');
|
|
374
|
+
const names = res.headers.getSetCookie().flatMap((line) => [...line.matchAll(/(?:^|,\s*)([A-Za-z0-9._-]+)=/g)].map((m) => m[1]));
|
|
375
|
+
if (names.join() !== LOGOUT_SET_COOKIE[kind].join())
|
|
376
|
+
throw fail(`Set-Cookie 与浏览器不一致:${names.join(', ')}`);
|
|
377
|
+
if (requireBody && compactJson(payload) !== (kind === 'passport' ? '{"result":1}' : '{"data":{"result":1}}'))
|
|
378
|
+
throw fail('响应体与浏览器不一致');
|
|
379
|
+
return payload;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* _assert_passport_logout_cookie:passport 登出的 Cookie 线必须与 Chrome 抓包的字段、顺序完全一致(缺字段也不行),
|
|
383
|
+
* webweapon 四件套的值形态也要对(PCLive、174 位 kwfv1、88 位 secToken、64 位十六进制 kwscode),否则不发登出请求。
|
|
384
|
+
*/
|
|
385
|
+
function assertPassportLogoutCookie(raw, profile) {
|
|
386
|
+
const pairs = raw.split('; ').filter((part) => part.includes('=')).map((part) => [part.slice(0, part.indexOf('=')), part.slice(part.indexOf('=') + 1)]);
|
|
387
|
+
const names = pairs.map(([name]) => name);
|
|
388
|
+
const values = (name) => pairs.filter(([key]) => key === name).map(([, value]) => value);
|
|
389
|
+
const fail = (message) => new SpecificAIError('UPSTREAM', `快手登出:${message},没有发登出请求`, { detail: { profile, cookies: names } });
|
|
390
|
+
if (names.join('; ') !== (SEQUENCES[profile] ?? []).join('; '))
|
|
391
|
+
throw fail('passport 登出的 Cookie 字段或顺序与浏览器不一致');
|
|
392
|
+
if (values('kwpsecproductname').join() !== PRODUCT_LIVE ||
|
|
393
|
+
values('kwfv1').map((v) => v.length).join() !== '174' ||
|
|
394
|
+
values('kwssectoken').map((v) => v.length).join() !== '88' ||
|
|
395
|
+
!/^[0-9a-f]{64}$/.test(values('kwscode')[0] ?? '')) {
|
|
396
|
+
throw fail('passport 登出的 webweapon Cookie 形态与浏览器不一致');
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* logout_session:直播页的“退出登录”——先 `POST id.kuaishou.com/pass/kuaishou/login/logout`,再
|
|
401
|
+
* `POST /live_api/baseuser/userLogout`。两条请求的请求头与 Cookie 线都是动作之前的快照;每一步按响应删掉
|
|
402
|
+
* passToken、userId 与直播票据(apply_live_logout)。上游对响应还检查 HTTP/1.1 与 Set-Cookie 的属性,这里不查。
|
|
403
|
+
*/
|
|
404
|
+
export async function liveLogout(ks) {
|
|
405
|
+
const referer = LIVE_LOGOUT_REFERER;
|
|
406
|
+
const present = ['userId', 'kuaishou.live.web_st', 'kuaishou.live.web_ph'].map((k) => Boolean(ks.s.cookies.get(k)));
|
|
407
|
+
const state = present.every(Boolean) ? 'authenticated' : present.some(Boolean) ? null : 'clean';
|
|
408
|
+
if (!state)
|
|
409
|
+
throw new SpecificAIError('UPSTREAM', '直播站票据不全(只有一部分登录 cookie),无法按浏览器的方式登出', { detail: { present } });
|
|
410
|
+
const kww = await ks.s.kww();
|
|
411
|
+
const passportCookie = await ks.s.cookieHeader(`live_logout_passport_${state}`);
|
|
412
|
+
const liveCookie = await ks.s.cookieHeader(`live_room_logout_${state}`);
|
|
413
|
+
assertPassportLogoutCookie(passportCookie, `live_logout_passport_${state}`);
|
|
414
|
+
const ph = buildHeaders('POST', 'login_pass_token');
|
|
415
|
+
ph.set('referer', referer);
|
|
416
|
+
ph.set('origin', LIVE);
|
|
417
|
+
ph.set('sec-fetch-site', 'same-site');
|
|
418
|
+
const [trace, span] = ks.s.nextLiveSentry(referer);
|
|
419
|
+
const lh = buildHeaders('GET', 'live');
|
|
420
|
+
lh.set('referer', referer);
|
|
421
|
+
lh.set('origin', LIVE);
|
|
422
|
+
lh.set('sentry-trace', `${trace}-${span}-0`);
|
|
423
|
+
lh.set('baggage', LIVE_SENTRY_BAGGAGE);
|
|
424
|
+
lh.set('kww', kww);
|
|
425
|
+
const passport = await checkLogout(await ks.send({ method: 'POST', url: `${ID_HOST}/pass/kuaishou/login/logout`, headers: ph.get(), cookie: passportCookie, body: PASSPORT_LOGOUT_BODY }), 'passport', state === 'clean');
|
|
426
|
+
ks.s.applyLiveLogout({ passport: true });
|
|
427
|
+
const live = await checkLogout(await ks.send({ method: 'POST', url: `${LIVE}/live_api/baseuser/userLogout`, headers: lh.get(), cookie: liveCookie, body: '' }), 'live', state === 'clean');
|
|
428
|
+
ks.s.applyLiveLogout({ live: true });
|
|
429
|
+
return { result: 1, state, passport, live };
|
|
430
|
+
}
|
|
431
|
+
export async function websocketInfo(ks, liveStreamId, eid) {
|
|
432
|
+
const [data] = await ks.live('GET', '/live_api/liveroom/websocketinfo', {
|
|
433
|
+
query: [['liveStreamId', liveStreamId]],
|
|
434
|
+
referer: roomReferer(eid),
|
|
435
|
+
cookieProfile: ks.roomProfile('authenticated'),
|
|
436
|
+
});
|
|
437
|
+
return data;
|
|
438
|
+
}
|
|
439
|
+
// ================================================================ 创作者中心:KuaishouPublishAPI
|
|
440
|
+
const CREATOR_AXIOS = { style: 'cp_creator_axios', withKww: false, creator: true };
|
|
441
|
+
export async function authorityAccountCurrent(ks) {
|
|
442
|
+
const result = await ks.cpPost('/rest/v2/creator/pc/authority/account/current', {}, { style: 'cp_creator_json', withKww: false, creator: true });
|
|
443
|
+
if (result?.result === 1 && result?.data?.ab?.enablePublish === true)
|
|
444
|
+
ks.cpAuthority = result;
|
|
445
|
+
return result;
|
|
446
|
+
}
|
|
447
|
+
/** require_publish_authority:上传之前复现发布页的账号权限门禁。 */
|
|
448
|
+
export async function requirePublishAuthority(ks) {
|
|
449
|
+
if (ks.cpAuthority)
|
|
450
|
+
return ks.cpAuthority;
|
|
451
|
+
const result = await authorityAccountCurrent(ks);
|
|
452
|
+
if (result?.result === 119120)
|
|
453
|
+
throw new SpecificAIError('UPSTREAM', '创作者中心:账号异常,暂时无法发布(result=119120)', { detail: { result: 119120 } });
|
|
454
|
+
ks.check(result, '创作者中心');
|
|
455
|
+
if (result?.data?.ab?.enablePublish !== true)
|
|
456
|
+
throw new SpecificAIError('UPSTREAM', '创作者中心:这个账号当前没有发布权限', { detail: { result: result?.result } });
|
|
457
|
+
return result;
|
|
458
|
+
}
|
|
459
|
+
export function notificationUnreadCount(ks) {
|
|
460
|
+
return ks.cpPost('/rest/v2/creator/pc/notification/unReadCountV3', {}, CREATOR_AXIOS);
|
|
461
|
+
}
|
|
462
|
+
/** 视频提交后浏览器跳到的作品管理页。 */
|
|
463
|
+
export const POST_PUBLISH_MANAGE_REFERER = `${CP}/article/manage/video?status=2&from=publish`;
|
|
464
|
+
const POST_PUBLISH_SITE = 'cp_post_publish_manage';
|
|
465
|
+
/**
|
|
466
|
+
* video_photo_list:作品管理页的作品列表(字段顺序固定)。postPublish 为发布后的管理页:
|
|
467
|
+
* Referer 是发布后管理页,Cookie 线序保留视频上传上下文的 kuaishou-vision 产品。
|
|
468
|
+
*/
|
|
469
|
+
export function videoPhotoList(ks, q, o = {}) {
|
|
470
|
+
return ks.cpPost('/rest/cp/works/v2/video/pc/photo/list', {
|
|
471
|
+
queryType: q.queryType,
|
|
472
|
+
cursor: q.cursor,
|
|
473
|
+
startTime: q.startTime,
|
|
474
|
+
endTime: q.endTime,
|
|
475
|
+
limit: q.limit,
|
|
476
|
+
timeRangeType: q.timeRangeType,
|
|
477
|
+
keyword: q.keyword,
|
|
478
|
+
}, o.postPublish ? { ...CREATOR_AXIOS, referer: POST_PUBLISH_MANAGE_REFERER, wireSite: POST_PUBLISH_SITE } : CREATOR_AXIOS);
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* 发布后管理页 photo/list 的这一行能不能拿去轮询(上游 video_photo_list 的 post_publish 判定):
|
|
482
|
+
* 只有一行、还在发布中(publishStatus=2、没有 workId)、未发布封面是本次 upload/finish 的 coverKey、publishId 是正整数。
|
|
483
|
+
*/
|
|
484
|
+
export function publishRefreshId(list, coverKey) {
|
|
485
|
+
const rows = list?.result === 1 ? (list.data?.list ?? []) : [];
|
|
486
|
+
const row = rows.length === 1 ? rows[0] : null;
|
|
487
|
+
if (!row || typeof row !== 'object' || row.publishStatus !== 2 || row.workId != null || row.unPublishCoverKey !== coverKey)
|
|
488
|
+
return null;
|
|
489
|
+
return Number.isInteger(row.publishId) && row.publishId > 0 ? row.publishId : null;
|
|
490
|
+
}
|
|
491
|
+
/** video_publish_refresh:用本次 photo/list 返回的 publishId 轮询发布状态(body 只有 ids 与 api_ph)。 */
|
|
492
|
+
export function videoPublishRefresh(ks, ids) {
|
|
493
|
+
return ks.cpPost('/rest/cp/works/v2/video/pc/publish/refresh', { ids }, { ...CREATOR_AXIOS, referer: POST_PUBLISH_MANAGE_REFERER, wireSite: POST_PUBLISH_SITE });
|
|
494
|
+
}
|
|
495
|
+
export const UPLOAD_TYPE_VIDEO = 1;
|
|
496
|
+
export const UPLOAD_TYPE_ATLAS = 10;
|
|
497
|
+
export function videoUploadPre(ks) {
|
|
498
|
+
return ks.cpPost('/rest/cp/works/v2/video/pc/upload/pre', { uploadType: UPLOAD_TYPE_VIDEO });
|
|
499
|
+
}
|
|
500
|
+
export async function videoUploadFinish(ks, token, fileName, fileType, fileLength) {
|
|
501
|
+
const result = await ks.cpPost('/rest/cp/works/v2/video/pc/upload/finish', { token, fileName, fileType, fileLength });
|
|
502
|
+
if (result?.result === 1)
|
|
503
|
+
ks.cpLastVideoFinish = { ...(result.data ?? {}) };
|
|
504
|
+
return result;
|
|
505
|
+
}
|
|
506
|
+
/** video_cover_report:submit 之前上报封面,bizKey 就是 upload/finish 返回的 coverKey。 */
|
|
507
|
+
export function videoCoverReport(ks, coverKey) {
|
|
508
|
+
return ks.cpPost('/rest/cp/works/v2/common/pc/report', { bizKey: coverKey, bizType: 1, data: '{}' });
|
|
509
|
+
}
|
|
510
|
+
export const PHOTO_STATUS = { public: 1, private: 2, friends: 4 };
|
|
511
|
+
/** publish_video 的当前 51 键 submit(走 sig4)。 */
|
|
512
|
+
export function videoSubmitBody(o) {
|
|
513
|
+
return {
|
|
514
|
+
fileId: o.fileId,
|
|
515
|
+
coverKey: o.coverKey,
|
|
516
|
+
coverTimeStamp: 0,
|
|
517
|
+
caption: o.caption.replace(/(\S)#/g, '$1 #'),
|
|
518
|
+
photoStatus: o.photoStatus,
|
|
519
|
+
coverType: 1,
|
|
520
|
+
coverTitle: '',
|
|
521
|
+
photoType: 0,
|
|
522
|
+
collectionId: 0,
|
|
523
|
+
publishTime: o.publishTime,
|
|
524
|
+
longitude: '',
|
|
525
|
+
latitude: '',
|
|
526
|
+
poiId: 0,
|
|
527
|
+
notifyResult: 0,
|
|
528
|
+
domain: '',
|
|
529
|
+
secondDomain: '',
|
|
530
|
+
coverCropped: false,
|
|
531
|
+
pkCoverKey: '',
|
|
532
|
+
profileCoverKey: '',
|
|
533
|
+
downloadType: 1,
|
|
534
|
+
disableNearbyShow: false,
|
|
535
|
+
allowSameFrame: true,
|
|
536
|
+
movieId: '',
|
|
537
|
+
openPrePreview: false,
|
|
538
|
+
declareInfo: { source: 0, platform: 0, time: 0, location: '', sourceId: 0, sourceName: '', statementId: 0 },
|
|
539
|
+
activityIds: [],
|
|
540
|
+
riseQuality: false,
|
|
541
|
+
chapters: null,
|
|
542
|
+
videoComposite: null,
|
|
543
|
+
useAiCaptionCover: false,
|
|
544
|
+
useAiCaption: false,
|
|
545
|
+
isUseIdealTime: false,
|
|
546
|
+
useAiCover: false,
|
|
547
|
+
kceInfo: '',
|
|
548
|
+
coverSize: '',
|
|
549
|
+
pkCoverTimeStamp: -1,
|
|
550
|
+
pkCoverType: 1,
|
|
551
|
+
pkCoverSize: '',
|
|
552
|
+
innerChannel: 0,
|
|
553
|
+
mediaId: o.mediaId,
|
|
554
|
+
videoInfoMeta: '',
|
|
555
|
+
triggerH265: false,
|
|
556
|
+
recTagIdList: [],
|
|
557
|
+
onvideoDuration: 0,
|
|
558
|
+
disallowRecreation: false,
|
|
559
|
+
previewUrlErrorMessage: '',
|
|
560
|
+
coPublishUser: [],
|
|
561
|
+
coPublishRole: 0,
|
|
562
|
+
extraInfo: '',
|
|
563
|
+
videoDuration: o.videoDuration,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
/** publish_video:先上报封面(必须 result=1),紧接着 submit。 */
|
|
567
|
+
export async function publishVideo(ks, o) {
|
|
568
|
+
const report = await videoCoverReport(ks, o.coverKey);
|
|
569
|
+
if (report?.result !== 1)
|
|
570
|
+
throw new SpecificAIError('UPSTREAM', '封面上报没有通过,已停止提交', { detail: { result: report?.result, message: report?.message } });
|
|
571
|
+
return ks.cpPost('/rest/cp/works/v2/video/pc/submit', videoSubmitBody(o));
|
|
572
|
+
}
|
|
573
|
+
export function atlasRealizeEntrance(ks) {
|
|
574
|
+
return ks.cpPost('/rest/cp/works/v2/video/pc/realize/entrance', { movieId: '' }, { upload: 'atlas' });
|
|
575
|
+
}
|
|
576
|
+
/** creator_activity_list 的两套字段顺序(initial / post_resume)。 */
|
|
577
|
+
export function activityList(ks, phase, upload) {
|
|
578
|
+
const body = phase === 'initial'
|
|
579
|
+
? { page: 1, count: 20, category: 0, rewardType: 0, sortType: 0, pageSource: 2 }
|
|
580
|
+
: { page: 1, count: 20, category: 0, sortType: 0, rewardType: 0, pageSource: 2 };
|
|
581
|
+
return ks.cpPost('/rest/v2/creator/activity/pc/list', body, { upload });
|
|
582
|
+
}
|
|
583
|
+
export const activityTab = (ks, upload) => ks.cpPost('/rest/v2/creator/activity/pc/tab', {}, { upload });
|
|
584
|
+
export const activityFilter = (ks, upload) => ks.cpPost('/rest/v2/creator/activity/pc/filter', {}, { upload });
|
|
585
|
+
export const collectionCanAddAtlas = (ks) => ks.cpPost('/rest/cp/works/v2/collection/canAddAtlas');
|
|
586
|
+
/** atlas_upload_pre:每张图各调一次;第一张不带 atlasId / fileId(JSON.stringify 丢掉 undefined)。 */
|
|
587
|
+
export function atlasUploadPre(ks, mime, atlasId, fileId) {
|
|
588
|
+
const body = {};
|
|
589
|
+
if (atlasId != null)
|
|
590
|
+
body.atlasId = atlasId;
|
|
591
|
+
if (fileId != null)
|
|
592
|
+
body.fileId = fileId;
|
|
593
|
+
body.uploadType = UPLOAD_TYPE_ATLAS;
|
|
594
|
+
body.pictureCount = 1;
|
|
595
|
+
body.fileExtendNames = compactJson([{ fileExtendName: mime }]);
|
|
596
|
+
return ks.cpPost('/rest/cp/works/atlas/pc/upload/pre', body);
|
|
597
|
+
}
|
|
598
|
+
export function atlasUploadSingleFinish(ks, fileId, atlasId, blobKey) {
|
|
599
|
+
return ks.cpPost('/rest/cp/works/atlas/pc/upload/single/finish', { fileId, atlasId, blobKey });
|
|
600
|
+
}
|
|
601
|
+
export function atlasSnapshotSave(ks, fileId, atlasId, caption = '', photoStatus = 1, publishTime = 0) {
|
|
602
|
+
return ks.cpPost('/rest/cp/works/atlas/pc/publishInfo/snapshot/save', {
|
|
603
|
+
fileId,
|
|
604
|
+
atlasId,
|
|
605
|
+
caption: caption.replace(/(\S)#/g, '$1 #'),
|
|
606
|
+
photoStatus,
|
|
607
|
+
longitude: '',
|
|
608
|
+
latitude: '',
|
|
609
|
+
declareInfo: {},
|
|
610
|
+
activityIds: [],
|
|
611
|
+
publishTime,
|
|
612
|
+
coPublishUser: [],
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
export function atlasUploadFinish(ks, fileId, atlasId, blobKeys) {
|
|
616
|
+
return ks.cpPost('/rest/cp/works/atlas/pc/upload/finish', { fileId, atlasId, blobKey: blobKeys }, { upload: 'atlas' });
|
|
617
|
+
}
|
|
618
|
+
export function publishAtlas(ks, o) {
|
|
619
|
+
return ks.cpPost('/rest/cp/works/atlas/pc/publish/submit', {
|
|
620
|
+
fileId: o.fileId,
|
|
621
|
+
atlasId: o.atlasId,
|
|
622
|
+
caption: o.caption.replace(/(\S)#/g, '$1 #'),
|
|
623
|
+
photoStatus: o.photoStatus,
|
|
624
|
+
longitude: '',
|
|
625
|
+
latitude: '',
|
|
626
|
+
declareInfo: {},
|
|
627
|
+
activityIds: [],
|
|
628
|
+
publishTime: o.publishTime,
|
|
629
|
+
coPublishUser: [],
|
|
630
|
+
useAiCaption: false,
|
|
631
|
+
coverType: o.coverType ?? 1,
|
|
632
|
+
}, { upload: 'atlas' });
|
|
633
|
+
}
|
|
634
|
+
// ================================================================ 分片上传:ksuploader
|
|
635
|
+
export const CHUNK_SIZE = 4 * 1024 * 1024;
|
|
636
|
+
const UPLOAD_ACCEPT = ACCEPT_AXIOS;
|
|
637
|
+
const CP_ROOT = `${CP}/`;
|
|
638
|
+
function uploadHeaders(path, contentRange = '') {
|
|
639
|
+
if (path === '/api/upload/fragment') {
|
|
640
|
+
return [
|
|
641
|
+
['referer', CP_ROOT],
|
|
642
|
+
['user-agent', UA],
|
|
643
|
+
['accept', UPLOAD_ACCEPT],
|
|
644
|
+
['content-type', 'application/octet-stream'],
|
|
645
|
+
['content-range', contentRange],
|
|
646
|
+
['accept-encoding', ACCEPT_ENCODING],
|
|
647
|
+
['accept-language', ACCEPT_LANGUAGE],
|
|
648
|
+
['origin', CP],
|
|
649
|
+
['sec-fetch-dest', 'empty'],
|
|
650
|
+
['sec-fetch-mode', 'cors'],
|
|
651
|
+
['sec-fetch-site', 'cross-site'],
|
|
652
|
+
];
|
|
653
|
+
}
|
|
654
|
+
return [
|
|
655
|
+
['user-agent', UA],
|
|
656
|
+
['accept', UPLOAD_ACCEPT],
|
|
657
|
+
['referer', CP_ROOT],
|
|
658
|
+
['accept-encoding', ACCEPT_ENCODING],
|
|
659
|
+
['accept-language', ACCEPT_LANGUAGE],
|
|
660
|
+
['origin', CP],
|
|
661
|
+
['sec-fetch-dest', 'empty'],
|
|
662
|
+
['sec-fetch-mode', 'cors'],
|
|
663
|
+
['sec-fetch-site', 'cross-site'],
|
|
664
|
+
];
|
|
665
|
+
}
|
|
666
|
+
/** endPoints 可能是裸域名,统一成 https 前缀、无尾斜杠。 */
|
|
667
|
+
export function uploadBase(endpoint) {
|
|
668
|
+
const e = endpoint.trim().replace(/\/+$/, '');
|
|
669
|
+
return /^https?:\/\//.test(e) ? e : `https://${e}`;
|
|
670
|
+
}
|
|
671
|
+
async function uploadCall(ks, method, base, path, query, body, contentRange) {
|
|
672
|
+
const res = await ks.http.request({ method, url: `${base}${path}`, query, headers: uploadHeaders(path, contentRange), body, cookies: false, timeout: 120 });
|
|
673
|
+
if (res.status >= 400)
|
|
674
|
+
throw new SpecificAIError('UPSTREAM', `上传失败:HTTP ${res.status}`, { detail: { status: res.status, path } });
|
|
675
|
+
const text = await res.text();
|
|
676
|
+
try {
|
|
677
|
+
return jsonLoads(text);
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
return { raw: text };
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
export const uploadResume = (ks, base, token) => uploadCall(ks, 'GET', base, '/api/upload/resume', [['upload_token', token]]);
|
|
684
|
+
export function uploadFragment(ks, base, token, index, chunk, start, total) {
|
|
685
|
+
const range = `bytes ${start}-${start + chunk.length - 1}/${total}`;
|
|
686
|
+
return uploadCall(ks, 'POST', base, '/api/upload/fragment', [
|
|
687
|
+
['upload_token', token],
|
|
688
|
+
['fragment_id', String(index)],
|
|
689
|
+
], chunk, range);
|
|
690
|
+
}
|
|
691
|
+
export const uploadComplete = (ks, base, token, count) => uploadCall(ks, 'POST', base, '/api/upload/complete', [
|
|
692
|
+
['fragment_count', count],
|
|
693
|
+
['upload_token', token],
|
|
694
|
+
], '');
|