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,133 @@
|
|
|
1
|
+
import { SpecificAIError } from '../../../core/errors.js';
|
|
2
|
+
import { md5Hex } from '../../../core/hash.js';
|
|
3
|
+
import { quote } from '../../../core/py.js';
|
|
4
|
+
import * as rand from '../../../core/rand.js';
|
|
5
|
+
import * as api from './api.js';
|
|
6
|
+
import { ORIGIN } from './profile.js';
|
|
7
|
+
import { imConversationRequest, imOuterRequest, imSendBody, imSendFrame, imUserInitRequest } from './wire.js';
|
|
8
|
+
import { frontierSign } from './jsrun.js';
|
|
9
|
+
/**
|
|
10
|
+
* 私信(上游 api/tiktok_web.py 的 IM 部分)。
|
|
11
|
+
*
|
|
12
|
+
* 上游要求浏览器会话里的 im_headers(32 个键的有序 map)、im_sequence_id、im_config_id 等运行时字段,缺了就拒绝。
|
|
13
|
+
* specific-ai 优先用会话 JSON 里的 browser_metrics;没有时 im_headers 按与 query 相同的浏览器画像补齐,
|
|
14
|
+
* sequence_id 从 10000 起递增,config_id 缺省时不发(与上游 `None` 时不写字段一致)。
|
|
15
|
+
*/
|
|
16
|
+
export const IM_HEADER_KEYS = [
|
|
17
|
+
'aid', 'app_name', 'channel', 'device_platform', 'device_id', 'region', 'priority_region', 'os', 'referer', 'root_referer', 'cookie_enabled',
|
|
18
|
+
'screen_width', 'screen_height', 'browser_language', 'browser_platform', 'browser_name', 'browser_version', 'browser_online', 'verifyFp',
|
|
19
|
+
'app_language', 'webcast_language', 'tz_name', 'is_page_visible', 'focus_state', 'is_fullscreen', 'history_len', 'user_is_login',
|
|
20
|
+
'data_collection_enabled', 'from_appID', 'locale', 'user_agent', 'Web-Sdk-Ms-Token',
|
|
21
|
+
];
|
|
22
|
+
/** Request.headers 的有序 map(上游 _im_header_map)。 */
|
|
23
|
+
export function imHeaders(t) {
|
|
24
|
+
const source = t.metrics.im_headers;
|
|
25
|
+
if (source && typeof source === 'object') {
|
|
26
|
+
const missing = IM_HEADER_KEYS.filter((k) => !(k in source));
|
|
27
|
+
if (missing.length)
|
|
28
|
+
throw new SpecificAIError('ERROR', `会话里的 im_headers 缺少字段:${missing.join(', ')}`);
|
|
29
|
+
return IM_HEADER_KEYS.map((k) => [k, String(source[k])]);
|
|
30
|
+
}
|
|
31
|
+
const values = {
|
|
32
|
+
aid: '1988',
|
|
33
|
+
app_name: 'tiktok_web',
|
|
34
|
+
channel: 'tiktok_web',
|
|
35
|
+
device_platform: 'web_pc',
|
|
36
|
+
device_id: t.deviceId,
|
|
37
|
+
region: t.region,
|
|
38
|
+
priority_region: t.priorityRegion,
|
|
39
|
+
os: 'windows',
|
|
40
|
+
referer: api.MESSAGES_PAGE,
|
|
41
|
+
root_referer: api.MESSAGES_PAGE,
|
|
42
|
+
cookie_enabled: 'true',
|
|
43
|
+
screen_width: '2560',
|
|
44
|
+
screen_height: '1440',
|
|
45
|
+
browser_language: 'zh-CN',
|
|
46
|
+
browser_platform: 'Win32',
|
|
47
|
+
browser_name: 'Mozilla',
|
|
48
|
+
browser_version: t.ua.split('Mozilla/').slice(1).join('Mozilla/'),
|
|
49
|
+
browser_online: 'true',
|
|
50
|
+
verifyFp: t.verifyFp,
|
|
51
|
+
app_language: 'zh-Hans',
|
|
52
|
+
webcast_language: 'zh-Hans',
|
|
53
|
+
tz_name: 'Asia/Shanghai',
|
|
54
|
+
is_page_visible: 'true',
|
|
55
|
+
focus_state: 'true',
|
|
56
|
+
is_fullscreen: 'false',
|
|
57
|
+
history_len: '4',
|
|
58
|
+
user_is_login: 'true',
|
|
59
|
+
data_collection_enabled: 'true',
|
|
60
|
+
from_appID: '1988',
|
|
61
|
+
locale: 'zh-Hans',
|
|
62
|
+
user_agent: t.ua,
|
|
63
|
+
'Web-Sdk-Ms-Token': t.msToken,
|
|
64
|
+
};
|
|
65
|
+
return IM_HEADER_KEYS.map((k) => [k, values[k]]);
|
|
66
|
+
}
|
|
67
|
+
/** 下一个 sequence_id:会话里给了就从它开始,之后每次 +1(存在凭证里)。 */
|
|
68
|
+
function nextSequence(t, key) {
|
|
69
|
+
const extra = t.ctx.credential.extra;
|
|
70
|
+
const stored = Number(extra[key] ?? t.metrics[key] ?? 10000);
|
|
71
|
+
extra[key] = stored + 1;
|
|
72
|
+
return stored;
|
|
73
|
+
}
|
|
74
|
+
/** 私信 HTTP 拉取请求的公共外层字段。 */
|
|
75
|
+
export function envelope(t) {
|
|
76
|
+
const config = t.metrics.im_config_id;
|
|
77
|
+
return { sequenceId: nextSequence(t, 'im_sequence_id'), deviceId: t.deviceId, headers: imHeaders(t), configId: config == null ? null : Number(config) };
|
|
78
|
+
}
|
|
79
|
+
/** cmd 203:私信页初次拉取(会话与最近消息)。翻页时 cursor 用上一页回包的 next_cursor。 */
|
|
80
|
+
export async function pullInit(t, cursor = 0) {
|
|
81
|
+
return api.postImProtobuf(t, '/v2/message/get_by_user_init', imUserInitRequest(cursor, envelope(t)));
|
|
82
|
+
}
|
|
83
|
+
/** cmd 301:一个会话的消息。 */
|
|
84
|
+
export async function pullConversation(t, c) {
|
|
85
|
+
const raw = imConversationRequest({ conversationId: c.conversationId, shortId: c.shortId, type: c.type, anchorIndex: c.anchorIndex ?? '0', direction: c.direction ?? 1, limit: c.limit ?? 50 }, envelope(t));
|
|
86
|
+
return api.postImProtobuf(t, '/v1/message/get_by_conversation', raw);
|
|
87
|
+
}
|
|
88
|
+
export function convCache(t) {
|
|
89
|
+
return (t.ctx.credential.extra.im_conversations ??= {});
|
|
90
|
+
}
|
|
91
|
+
// ---------------------------------------------------------------- WebSocket
|
|
92
|
+
/** WS 的 access_key:md5("9" + app_key + wid + 盐),wid 来自 privacy config(上游 build_im_access_key)。 */
|
|
93
|
+
export async function imWsUrl(t) {
|
|
94
|
+
let wid = String(t.metrics.im_wid ?? '');
|
|
95
|
+
if (!wid) {
|
|
96
|
+
const body = await api.cookiePrivacyConfig(t);
|
|
97
|
+
wid = String(body?.body?.consent?.wid ?? '');
|
|
98
|
+
if (!wid)
|
|
99
|
+
throw new SpecificAIError('UPSTREAM', 'TikTok privacy config 缺少 body.consent.wid');
|
|
100
|
+
}
|
|
101
|
+
const accessKey = md5Hex(`9e1bd35ec9db7b8d846de66ed140b1ad9${wid}f8a69f1719916z`);
|
|
102
|
+
const ttwid = t.ttwid;
|
|
103
|
+
if (!ttwid)
|
|
104
|
+
throw new SpecificAIError('AUTH_REQUIRED', '私信 WebSocket 缺少 ttwid cookie', { hint: 'specific-ai tiktok auth login' });
|
|
105
|
+
return (`wss://im-ws-${t.region.toLowerCase()}.tiktok.com/ws/v2?device_platform=web&version_code=fws_1.0.0&access_key=${accessKey}` +
|
|
106
|
+
`&fpid=9&aid=1459&ttwid=${quote(ttwid, '|~-._')}&xsack=1&xaack=1&xsqos=0`);
|
|
107
|
+
}
|
|
108
|
+
/** cmd 100 的完整帧:Request 里带现签的 ticket-guard,帧头带 frontierSign(md5(Request))(上游 send_im_message)。 */
|
|
109
|
+
export async function buildSendFrame(t, c) {
|
|
110
|
+
const normal = imHeaders(t);
|
|
111
|
+
const guard = t.ticketGuard('/ws/v2');
|
|
112
|
+
const requestHeaders = [...normal];
|
|
113
|
+
for (const [k, v] of Object.entries(guard)) {
|
|
114
|
+
const i = requestHeaders.findIndex(([key]) => key === k);
|
|
115
|
+
if (i >= 0)
|
|
116
|
+
requestHeaders[i] = [k, v];
|
|
117
|
+
else
|
|
118
|
+
requestHeaders.push([k, v]);
|
|
119
|
+
}
|
|
120
|
+
const clientMessageId = rand.uuid4();
|
|
121
|
+
const body = imSendBody({ conversationId: c.conversationId, shortId: c.shortId, type: c.type, text: c.text, clientMessageId });
|
|
122
|
+
const sequenceId = nextSequence(t, 'im_ws_sequence_id');
|
|
123
|
+
const request = imOuterRequest(100, body, { sequenceId, deviceId: t.deviceId, devicePlatform: 'web', headers: requestHeaders, authType: 1 });
|
|
124
|
+
const marker = await frontierSign({
|
|
125
|
+
cookie: t.device.document_cookie || t.cookieStr,
|
|
126
|
+
userAgent: t.ua,
|
|
127
|
+
referer: String(t.metrics.im_page || `${ORIGIN}/messages`),
|
|
128
|
+
metrics: t.metrics,
|
|
129
|
+
stub: md5Hex(request),
|
|
130
|
+
});
|
|
131
|
+
const frame = imSendFrame({ request, sequenceId, logId: rand.now(), xBogus: marker, frameHeaders: normal });
|
|
132
|
+
return { frame, request, body, clientMessageId, sequenceId };
|
|
133
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { SpecificAIError } from '../../../core/errors.js';
|
|
4
|
+
import { PACKAGE_ROOT, staticFile } from '../../../core/paths.js';
|
|
5
|
+
import * as rand from '../../../core/rand.js';
|
|
6
|
+
/**
|
|
7
|
+
* 上游的两个签名运行器(AGENTS 7.4 的子进程退路):
|
|
8
|
+
* - static/tiktok/signing/env/sign.js:WebMssdk 的 frontierSign(直播 / 私信 WebSocket 的 16 位 X-Bogus);
|
|
9
|
+
* - static/tiktok/shop_bsid/sign.js:TikTok Shop 的 OEC Lucifer BSID。
|
|
10
|
+
*
|
|
11
|
+
* 两者都改写进程级全局(global.window、Function.prototype.toString、隐藏 process 等),放不进 node:vm,
|
|
12
|
+
* 所以和上游一样每次起一个 node 子进程,按包内路径运行原样复制的 JS。
|
|
13
|
+
*
|
|
14
|
+
* 确定性模式(对拍)下预加载 scripts/golden/tiktok/node_determinism.cjs,与上游对拍时用的是同一个文件。
|
|
15
|
+
*/
|
|
16
|
+
async function runNode(script, input, env = {}) {
|
|
17
|
+
const deterministic = rand.isDeterministic();
|
|
18
|
+
const extra = deterministic
|
|
19
|
+
? {
|
|
20
|
+
NODE_OPTIONS: `--require "${join(PACKAGE_ROOT, 'scripts', 'golden', 'tiktok', 'node_determinism.cjs').replaceAll('\\', '/')}"`,
|
|
21
|
+
SPECIFIC_AI_GOLDEN_SEED: String(rand.DEFAULT_SEED),
|
|
22
|
+
SPECIFIC_AI_GOLDEN_NOW: String(rand.now()),
|
|
23
|
+
}
|
|
24
|
+
: { NODE_OPTIONS: '' };
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const child = spawn(process.execPath, [script], {
|
|
27
|
+
env: { ...process.env, ...extra, ...env },
|
|
28
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
29
|
+
windowsHide: true,
|
|
30
|
+
});
|
|
31
|
+
const out = [];
|
|
32
|
+
const err = [];
|
|
33
|
+
const timer = setTimeout(() => child.kill(), 30_000);
|
|
34
|
+
child.stdout.on('data', (d) => out.push(d));
|
|
35
|
+
child.stderr.on('data', (d) => err.push(d));
|
|
36
|
+
child.on('error', (e) => {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
reject(new SpecificAIError('ERROR', `签名进程启动失败:${e.message}`));
|
|
39
|
+
});
|
|
40
|
+
child.on('close', (code) => {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
const stdout = Buffer.concat(out).toString('utf8');
|
|
43
|
+
if (code !== 0 && !stdout.trim()) {
|
|
44
|
+
// 运行器的 stderr 可能带着 cookie,只取最后一行错误类型
|
|
45
|
+
const reason = Buffer.concat(err).toString('utf8').trim().split(/\r?\n/).reverse().find((l) => /(Error|error):|is required|failed|length/.test(l)) ?? '';
|
|
46
|
+
reject(new SpecificAIError('ERROR', `签名进程失败${reason ? `:${reason.slice(0, 200)}` : ''}`));
|
|
47
|
+
}
|
|
48
|
+
else
|
|
49
|
+
resolve(stdout);
|
|
50
|
+
});
|
|
51
|
+
child.stdin.end(input);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
export async function frontierSign(o) {
|
|
55
|
+
const request = { mode: 'frontier_sign', url: o.referer, cookie: o.cookie, user_agent: o.userAgent, referer: o.referer, metrics: o.metrics ?? {}, headers: {} };
|
|
56
|
+
if (o.stub != null) {
|
|
57
|
+
if (!/^[0-9a-f]{32}$/.test(o.stub))
|
|
58
|
+
throw new SpecificAIError('ERROR', 'IM X-MS-STUB 必须为 32 位小写 MD5');
|
|
59
|
+
request.stub = o.stub;
|
|
60
|
+
}
|
|
61
|
+
const stdout = await runNode(staticFile('tiktok', 'signing/env/sign.js'), JSON.stringify(request) + '\n');
|
|
62
|
+
const replies = stdout.split(/\r?\n/).filter((l) => l.startsWith('{"ok":'));
|
|
63
|
+
let result = {};
|
|
64
|
+
try {
|
|
65
|
+
result = replies.length ? JSON.parse(replies[replies.length - 1]) : {};
|
|
66
|
+
}
|
|
67
|
+
catch { }
|
|
68
|
+
const marker = result?.result?.values?.['X-Bogus'];
|
|
69
|
+
if (!result.ok || typeof marker !== 'string' || marker.length !== 16) {
|
|
70
|
+
throw new SpecificAIError('ERROR', `本地 frontierSign 未产生长度 16 的 X-Bogus${result.error ? `:${String(result.error).slice(0, 200)}` : ''}`);
|
|
71
|
+
}
|
|
72
|
+
return marker;
|
|
73
|
+
}
|
|
74
|
+
// ---------------------------------------------------------------- Shop BSID(signing/shop_bsid.py ShopBSIDSigner)
|
|
75
|
+
export const SHOP_BSID_LENGTH = 382;
|
|
76
|
+
const BROWSER_MS_TOKEN_LENGTHS = [144, 152];
|
|
77
|
+
export async function shopSign(o) {
|
|
78
|
+
const oec = /(?:^|;\s*)oec_lucifer=([^;]+)/.exec(o.cookie)?.[1];
|
|
79
|
+
if (!oec || !/^[0-9a-fA-F]{160}$/.test(oec)) {
|
|
80
|
+
throw new SpecificAIError('AUTH_REQUIRED', '当前 Cookie 缺少浏览器生成的 160 位 oec_lucifer(在 shop.tiktok.com 打开任一商品页后重新导出 Cookie)', {
|
|
81
|
+
hint: 'specific-ai tiktok auth login --cookie @tiktok-session.json',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const msToken = [...o.cookie.matchAll(/(?:^|;\s*)msToken=([^;]+)/g)].map((m) => decodeURIComponent(m[1])).find((v) => BROWSER_MS_TOKEN_LENGTHS.includes(v.length)) ?? '';
|
|
85
|
+
if (!/^[0-9A-Za-z_-]+={0,2}$/.test(msToken)) {
|
|
86
|
+
throw new SpecificAIError('AUTH_REQUIRED', '当前 Cookie 缺少 Shop 请求使用的 144/152 位 msToken(在 shop.tiktok.com 打开商品页后重新导出 Cookie)', {
|
|
87
|
+
hint: 'specific-ai tiktok auth login --cookie @tiktok-session.json',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (!o.userAgent)
|
|
91
|
+
throw new SpecificAIError('ERROR', 'Shop X-Bogus 纯算需要完整 user-agent');
|
|
92
|
+
const sep = o.url.includes('?') ? '&' : '?';
|
|
93
|
+
// Chrome 的 pre-sign URL 里 base64 的 == 原样保留
|
|
94
|
+
const signingUrl = `${o.url}${sep}msToken=${encodeURIComponent(msToken).replace(/%3D/g, '=')}`;
|
|
95
|
+
const payload = {
|
|
96
|
+
url: signingUrl,
|
|
97
|
+
method: o.method.toUpperCase(),
|
|
98
|
+
headers: o.headers,
|
|
99
|
+
body: o.body,
|
|
100
|
+
cookie: o.cookie,
|
|
101
|
+
expected_length: SHOP_BSID_LENGTH,
|
|
102
|
+
expected_ms_token_length: msToken.length,
|
|
103
|
+
x_bogus: o.xBogus(msToken),
|
|
104
|
+
};
|
|
105
|
+
const stdout = await runNode(staticFile('tiktok', 'shop_bsid/sign.js'), JSON.stringify(payload), { TIKTOK_BSID_QUIET: '1' });
|
|
106
|
+
let result;
|
|
107
|
+
try {
|
|
108
|
+
result = JSON.parse(stdout);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
throw new SpecificAIError('ERROR', 'Shop BSID 运行器返回了非 JSON 数据');
|
|
112
|
+
}
|
|
113
|
+
const bsid = String(result.bsid ?? '');
|
|
114
|
+
if (bsid.length !== SHOP_BSID_LENGTH || !/^[0-9a-f]+$/.test(bsid) || String(result.length) !== String(SHOP_BSID_LENGTH)) {
|
|
115
|
+
throw new SpecificAIError('ERROR', 'Shop BSID 输出格式或长度与 Chrome 不一致');
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
signed_url: String(result.signed_url ?? ''),
|
|
119
|
+
bsid,
|
|
120
|
+
length: SHOP_BSID_LENGTH,
|
|
121
|
+
pre_sign_lengths: result.pre_sign_lengths ?? {},
|
|
122
|
+
pre_sign_url_length: Number(result.pre_sign_url_length ?? 0),
|
|
123
|
+
pre_sign_order: result.pre_sign_order ?? [],
|
|
124
|
+
pre_sign_url: String(result.pre_sign_url ?? ''),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import * as n from '../../../core/normalize.js';
|
|
2
|
+
import { jsonLoads } from '../../../core/py.js';
|
|
3
|
+
import { ORIGIN, SHOP } from './profile.js';
|
|
4
|
+
/** TikTok 原始对象 → 归一化类型(AGENTS 6.2)。 */
|
|
5
|
+
export const userUrl = (uniqueId) => (uniqueId ? `${ORIGIN}/@${uniqueId}` : null);
|
|
6
|
+
export const itemUrl = (uniqueId, id, photo = false) => `${ORIGIN}/@${uniqueId || '_'}/${photo ? 'photo' : 'video'}/${id}`;
|
|
7
|
+
export const liveUrl = (uniqueId) => (uniqueId ? `${ORIGIN}/@${uniqueId}/live` : null);
|
|
8
|
+
export const productUrl = (id) => `${SHOP}/view/product/${id}`;
|
|
9
|
+
export function ref(id, name, uniqueId) {
|
|
10
|
+
return n.userRef({ id, name: name || uniqueId, url: userUrl(uniqueId) });
|
|
11
|
+
}
|
|
12
|
+
const first = (v) => (Array.isArray(v) ? n.url(v[0]) : n.url(v));
|
|
13
|
+
/** 作品(itemStruct:视频页 hydration、搜索、推荐、主页列表都用这个结构)。 */
|
|
14
|
+
export function item(v) {
|
|
15
|
+
const a = v.author ?? {};
|
|
16
|
+
const photo = Boolean(v.imagePost);
|
|
17
|
+
const s = { ...(v.stats ?? {}), ...(v.statsV2 ?? {}) };
|
|
18
|
+
return n.item({
|
|
19
|
+
id: n.id(v.id),
|
|
20
|
+
kind: photo ? 'image' : 'video',
|
|
21
|
+
url: itemUrl(a.uniqueId, v.id, photo),
|
|
22
|
+
title: n.str(v.imagePost?.title),
|
|
23
|
+
text: n.str(v.desc),
|
|
24
|
+
author: ref(a.id, a.nickname, a.uniqueId),
|
|
25
|
+
created_at: n.time(v.createTime),
|
|
26
|
+
cover: n.url(v.video?.cover || v.video?.originCover) ?? first(v.imagePost?.cover?.imageURL?.urlList),
|
|
27
|
+
media: itemMedia(v),
|
|
28
|
+
stats: {
|
|
29
|
+
views: n.count(s.playCount),
|
|
30
|
+
likes: n.count(s.diggCount),
|
|
31
|
+
comments: n.count(s.commentCount),
|
|
32
|
+
collects: n.count(s.collectCount),
|
|
33
|
+
shares: n.count(s.shareCount),
|
|
34
|
+
},
|
|
35
|
+
status: v.privateItem || v.secret ? 'private' : null,
|
|
36
|
+
}, v);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 可播放 / 下载的媒体:视频取 playAddr(同源 h264),图文取每张图。
|
|
40
|
+
* 背景音乐:图文总是附上(图文的声音只在音乐里);视频只在 `withMusic` 时附上(`item media` 要,Item.media 与下载不要)。
|
|
41
|
+
*/
|
|
42
|
+
export function itemMedia(v, withMusic = false) {
|
|
43
|
+
const out = [];
|
|
44
|
+
const images = v.imagePost?.images ?? [];
|
|
45
|
+
if (images.length) {
|
|
46
|
+
images.forEach((img, i) => {
|
|
47
|
+
const url = first(img.imageURL?.urlList);
|
|
48
|
+
if (url)
|
|
49
|
+
out.push(n.media({ id: String(i + 1), type: 'image', url, width: n.count(img.imageWidth), height: n.count(img.imageHeight) }, img));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else if (v.video) {
|
|
53
|
+
const url = n.url(v.video.playAddr) ?? n.url(v.video.downloadAddr) ?? first(v.video.bitrateInfo?.[0]?.PlayAddr?.UrlList);
|
|
54
|
+
if (url) {
|
|
55
|
+
out.push(n.media({ id: n.id(v.video.id || v.id), type: 'video', url, width: n.count(v.video.width), height: n.count(v.video.height), duration: n.seconds(v.video.duration) }, v.video));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if ((images.length || withMusic) && v.music?.playUrl)
|
|
59
|
+
out.push(n.media({ id: n.id(v.music.id), type: 'audio', url: n.url(v.music.playUrl), duration: n.seconds(v.music.duration) }, v.music));
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
/** 用户(user-detail 的 userInfo,或列表里的 {user, stats})。 */
|
|
63
|
+
export function user(info) {
|
|
64
|
+
const u = info.user ?? info;
|
|
65
|
+
const s = { ...(info.stats ?? {}), ...(info.statsV2 ?? {}) };
|
|
66
|
+
return n.user({
|
|
67
|
+
id: n.id(u.id ?? u.uid),
|
|
68
|
+
name: n.str(u.nickname ?? u.nickName),
|
|
69
|
+
handle: n.str(u.uniqueId ?? u.unique_id),
|
|
70
|
+
avatar: n.url(u.avatarLarger ?? u.avatarMedium ?? u.avatarThumb),
|
|
71
|
+
url: userUrl(u.uniqueId ?? u.unique_id),
|
|
72
|
+
bio: n.str(u.signature),
|
|
73
|
+
stats: {
|
|
74
|
+
followers: n.count(s.followerCount),
|
|
75
|
+
following: n.count(s.followingCount),
|
|
76
|
+
items: n.count(s.videoCount),
|
|
77
|
+
likes: n.count(s.heartCount ?? s.heart),
|
|
78
|
+
},
|
|
79
|
+
}, info);
|
|
80
|
+
}
|
|
81
|
+
export function comment(v, itemId) {
|
|
82
|
+
const u = v.user ?? {};
|
|
83
|
+
const parent = v.reply_id && v.reply_id !== '0' ? n.id(v.reply_id) : null;
|
|
84
|
+
return n.comment({
|
|
85
|
+
id: n.id(v.cid),
|
|
86
|
+
item_id: n.id(v.aweme_id ?? itemId),
|
|
87
|
+
parent_id: parent,
|
|
88
|
+
author: ref(u.uid, u.nickname, u.unique_id),
|
|
89
|
+
text: String(v.text ?? ''),
|
|
90
|
+
created_at: n.time(v.create_time),
|
|
91
|
+
stats: { likes: n.count(v.digg_count), replies: n.count(v.reply_comment_total) },
|
|
92
|
+
}, v);
|
|
93
|
+
}
|
|
94
|
+
/** 商品评价(Shop PDP 的 product_reviews 项)。 */
|
|
95
|
+
export function review(v, productId) {
|
|
96
|
+
return n.comment({
|
|
97
|
+
id: n.id(v.review_id ?? v.id),
|
|
98
|
+
item_id: productId,
|
|
99
|
+
author: n.userRef({ id: v.review_user?.user_id ?? v.user_id ?? v.review_user?.name, name: v.review_user?.name ?? v.user_name }),
|
|
100
|
+
text: String(v.review_text ?? v.display_text ?? v.text ?? ''),
|
|
101
|
+
created_at: n.time(v.review_timestamp ?? v.review_time ?? v.create_time),
|
|
102
|
+
stats: { likes: n.count(v.like_count ?? v.helpful_count), replies: null },
|
|
103
|
+
}, v);
|
|
104
|
+
}
|
|
105
|
+
/** 商品(Shop PDP SSR 的 product_info.component_data)。 */
|
|
106
|
+
export function product(d) {
|
|
107
|
+
const p = d.product_info?.product_model ?? {};
|
|
108
|
+
const promo = d.product_info?.promotion_model ?? d.promotion_model ?? {};
|
|
109
|
+
const seller = d.product_info?.seller_model ?? d.seller_model ?? d.shop_info ?? {};
|
|
110
|
+
const price = promo.promotion_product_price ?? promo.min_price ?? p.min_price ?? {};
|
|
111
|
+
const images = p.images ?? [];
|
|
112
|
+
const amount = price.sale_price_decimal ?? price.real_price ?? price.min_sku_price ?? price.price_val;
|
|
113
|
+
return n.item({
|
|
114
|
+
id: n.id(p.product_id),
|
|
115
|
+
kind: 'goods',
|
|
116
|
+
url: productUrl(p.product_id),
|
|
117
|
+
title: n.str(p.name ?? p.title),
|
|
118
|
+
text: n.str(p.description_text ?? null),
|
|
119
|
+
author: n.userRef({ id: seller.seller_id ?? seller.shop_id, name: seller.name ?? seller.shop_name }),
|
|
120
|
+
cover: first(images[0]?.url_list),
|
|
121
|
+
media: images.flatMap((img, i) => {
|
|
122
|
+
const url = first(img.url_list);
|
|
123
|
+
return url ? [n.media({ id: String(i + 1), type: 'image', url, width: n.count(img.width), height: n.count(img.height) }, img)] : [];
|
|
124
|
+
}),
|
|
125
|
+
stats: { comments: n.count(d.review_info?.total_reviews ?? p.review_count), views: null, likes: null, collects: null, shares: null },
|
|
126
|
+
price: n.price(amount, price.currency ?? price.currency_name ?? 'USD'),
|
|
127
|
+
status: p.status === 1 || p.status == null ? 'on_sale' : 'off_shelf',
|
|
128
|
+
}, d);
|
|
129
|
+
}
|
|
130
|
+
export function folder(v) {
|
|
131
|
+
return n.folder({ id: n.id(v.collectionId), name: String(v.name ?? ''), count: n.count(v.total), url: v.userName ? `${ORIGIN}/@${v.userName}/collection/${encodeURIComponent(v.name ?? '')}-${v.collectionId}` : null }, v);
|
|
132
|
+
}
|
|
133
|
+
export function series(v) {
|
|
134
|
+
const id = n.id(v.mixId ?? v.id);
|
|
135
|
+
const creator = v.creator?.uniqueId;
|
|
136
|
+
return n.folder({ id, name: String(v.mixName ?? v.name ?? ''), count: n.count(v.videoCount), url: creator ? `${ORIGIN}/@${creator}/playlist/${encodeURIComponent(v.mixName ?? v.name ?? '')}-${id}` : null }, v);
|
|
137
|
+
}
|
|
138
|
+
// ---------------------------------------------------------------- 直播
|
|
139
|
+
/** /api-live/user/room 的 data:user + liveRoom。status 2 为直播中。 */
|
|
140
|
+
export function liveRoom(d) {
|
|
141
|
+
const u = d.user ?? {};
|
|
142
|
+
const r = d.liveRoom ?? {};
|
|
143
|
+
return n.live({
|
|
144
|
+
id: n.id(u.roomId || r.roomId),
|
|
145
|
+
url: liveUrl(u.uniqueId),
|
|
146
|
+
title: n.str(r.title),
|
|
147
|
+
status: Number(r.status ?? u.status) === 2 ? 'live' : 'offline',
|
|
148
|
+
host: ref(u.id, u.nickname, u.uniqueId),
|
|
149
|
+
cover: n.url(r.coverUrl),
|
|
150
|
+
stats: { viewers: n.count(r.liveRoomStats?.userCount) },
|
|
151
|
+
}, d);
|
|
152
|
+
}
|
|
153
|
+
/** webcast 接口的房间对象(feed、搜索):snake_case。 */
|
|
154
|
+
export function webcastRoom(r) {
|
|
155
|
+
const o = r.owner ?? {};
|
|
156
|
+
return n.live({
|
|
157
|
+
id: n.id(r.id_str ?? r.id),
|
|
158
|
+
url: liveUrl(o.display_id),
|
|
159
|
+
title: n.str(r.title),
|
|
160
|
+
status: Number(r.status) === 2 ? 'live' : 'offline',
|
|
161
|
+
host: ref(o.id_str ?? o.id, o.nickname, o.display_id),
|
|
162
|
+
cover: first(r.cover?.url_list),
|
|
163
|
+
stats: { viewers: n.count(r.user_count ?? r.stats?.total_user) },
|
|
164
|
+
}, r);
|
|
165
|
+
}
|
|
166
|
+
/** 画质的顺序:原画在前,纯音频(ao)在最后。 */
|
|
167
|
+
const QUALITIES = ['origin', 'uhd', 'hd', 'sd', 'ld', 'md', 'ao'];
|
|
168
|
+
const PROTOCOLS = ['flv', 'hls', 'cmaf', 'dash', 'lls'];
|
|
169
|
+
function parseJson(v) {
|
|
170
|
+
if (typeof v !== 'string')
|
|
171
|
+
return v ?? null;
|
|
172
|
+
try {
|
|
173
|
+
return jsonLoads(v);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* 拉流地址(上游没有解析)。两种来源:
|
|
181
|
+
* - `pull_data.stream_data`:JSON 串,`data.<画质>.main.{flv,hls,...}`,`sdk_params`(JSON 串)里有 resolution。
|
|
182
|
+
* /api-live/user/room 在 `liveRoom.streamData`,webcast 房间对象在 `stream_url.live_core_sdk_data`;
|
|
183
|
+
* - webcast 房间对象的 `stream_url.flv_pull_url`(画质 → 地址)、`hls_pull_url_map`、`hls_pull_url`、`rtmp_pull_url`。
|
|
184
|
+
*/
|
|
185
|
+
export function liveStreams(source) {
|
|
186
|
+
const out = [];
|
|
187
|
+
const seen = new Set();
|
|
188
|
+
const push = (id, url, raw, size) => {
|
|
189
|
+
const u = n.url(url);
|
|
190
|
+
if (!u || seen.has(u))
|
|
191
|
+
return;
|
|
192
|
+
seen.add(u);
|
|
193
|
+
const [w, h] = /^(\d+)x(\d+)$/.exec(String(size ?? ''))?.slice(1) ?? [];
|
|
194
|
+
out.push(n.media({ id, type: id.startsWith('ao-') ? 'audio' : 'video', url: u, width: n.count(w), height: n.count(h) }, raw));
|
|
195
|
+
};
|
|
196
|
+
// 可能是 {pull_data}、{live_core_sdk_data},也可能已经是 stream_data 本身(JSON 串或解析后的对象)
|
|
197
|
+
const pull = source?.pull_data ?? source?.live_core_sdk_data?.pull_data;
|
|
198
|
+
const data = parseJson(pull?.stream_data ?? (typeof source === 'string' || source?.data ? source : null))?.data;
|
|
199
|
+
if (data && typeof data === 'object') {
|
|
200
|
+
const keys = Object.keys(data).sort((a, b) => (QUALITIES.indexOf(a) + 1 || 99) - (QUALITIES.indexOf(b) + 1 || 99));
|
|
201
|
+
for (const q of keys) {
|
|
202
|
+
const main = data[q]?.main ?? {};
|
|
203
|
+
const size = parseJson(main.sdk_params)?.resolution;
|
|
204
|
+
for (const p of PROTOCOLS)
|
|
205
|
+
push(`${q}-${p}`, main[p], data[q], size);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (const [q, url] of Object.entries(source?.flv_pull_url ?? {}))
|
|
209
|
+
push(`${q}-flv`, url, source);
|
|
210
|
+
for (const [q, url] of Object.entries(source?.hls_pull_url_map ?? {}))
|
|
211
|
+
push(`${q}-hls`, url, source);
|
|
212
|
+
push('default-hls', source?.hls_pull_url, source);
|
|
213
|
+
push('default-rtmp', source?.rtmp_pull_url, source);
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
export function gift(g) {
|
|
217
|
+
return n.gift({ id: n.id(g.id), name: String(g.name ?? ''), price: g.diamond_count != null ? { amount: Number(g.diamond_count), currency: 'TIKTOK_DIAMOND' } : null }, g);
|
|
218
|
+
}
|
|
219
|
+
export function rank(v, i) {
|
|
220
|
+
const u = v.user ?? {};
|
|
221
|
+
const r = ref(u.id_str ?? u.id, u.nickname, u.display_id);
|
|
222
|
+
return r ? n.rank({ rank: Number(v.rank ?? i + 1), user: r, score: n.count(v.score) }, v) : null;
|
|
223
|
+
}
|
|
224
|
+
function liveUserRef(u) {
|
|
225
|
+
return u && u.id !== '0' ? ref(u.id, u.nickname, u.display_id) : null;
|
|
226
|
+
}
|
|
227
|
+
/** 直播 protobuf 事件 → Event;只保留上游关心的 chat / like / gift。 */
|
|
228
|
+
export function liveEvent(e) {
|
|
229
|
+
if (e.type === 'chat')
|
|
230
|
+
return n.event({ type: 'chat', user: liveUserRef(e.user), text: e.text ?? '' }, e);
|
|
231
|
+
if (e.type === 'like')
|
|
232
|
+
return n.event({ type: 'like', user: liveUserRef(e.user), text: e.count != null ? String(e.count) : null }, e);
|
|
233
|
+
if (e.type === 'gift')
|
|
234
|
+
return n.event({ type: 'gift', user: liveUserRef(e.user), gift: { name: e.gift?.name || String(e.gift_id ?? ''), count: e.combo_count || 1 } }, e);
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
export function categories(tabs) {
|
|
238
|
+
return tabs.flatMap((t) => [
|
|
239
|
+
n.category({ id: String(t.tab_type ?? t.tab_name), name: String(t.tab_name ?? t.tab_type) }, t),
|
|
240
|
+
...(t.sub_tabs ?? []).map((s) => n.category({ id: String(s.tab_type ?? s.tab_name), name: String(s.tab_name ?? s.tab_type), parent_id: String(t.tab_type ?? t.tab_name) }, s)),
|
|
241
|
+
]);
|
|
242
|
+
}
|
|
243
|
+
// ---------------------------------------------------------------- 通知与私信
|
|
244
|
+
const NOTICE_TYPES = { 31: 'comment', 45: 'comment', 33: 'follow', 41: 'like', 60: 'like', 62: 'like', 43: 'mention', 44: 'mention' };
|
|
245
|
+
/**
|
|
246
|
+
* 通知:动态(group 500,按 follow / digg / comment / at 分)与系统通知(group 661,`system` 为 true)。
|
|
247
|
+
* 模板通知(template_notice)的标题在 notice.title_template.title,正文在 notice.content;动态里的转发通知也用模板。
|
|
248
|
+
*/
|
|
249
|
+
export function notice(v, system = false) {
|
|
250
|
+
const template = v.template_notice?.notice;
|
|
251
|
+
const type = system ? 'system' : (NOTICE_TYPES[Number(v.type)] ?? (v.follow ? 'follow' : v.digg ? 'like' : v.comment ? 'comment' : v.at ? 'mention' : 'system'));
|
|
252
|
+
const body = v.follow ?? v.digg ?? v.comment ?? v.at ?? v.system ?? template ?? {};
|
|
253
|
+
const u = body.from_user?.[0] ?? body.user ?? body.from_user ?? v.from_user ?? {};
|
|
254
|
+
const aweme = body.aweme ?? body.comment?.aweme ?? null;
|
|
255
|
+
const text = template ? [template.title_template?.title, template.content].filter(Boolean).join('\n') : (body.content ?? body.comment?.text ?? body.title ?? v.content);
|
|
256
|
+
return n.notice({
|
|
257
|
+
id: n.id(v.nid_str ?? v.nid),
|
|
258
|
+
type,
|
|
259
|
+
user: ref(u.uid ?? u.id, u.nickname, u.unique_id ?? u.uniqueId),
|
|
260
|
+
target: aweme?.aweme_id ? { id: n.id(aweme.aweme_id), url: itemUrl(aweme.author?.unique_id, aweme.aweme_id) } : null,
|
|
261
|
+
text: n.str(text),
|
|
262
|
+
created_at: n.time(v.create_time),
|
|
263
|
+
}, v);
|
|
264
|
+
}
|
|
265
|
+
/** 私信文本消息:拉取回包(PulledMessage)和 WS 推送(ImTextMessage)的字段相同。 */
|
|
266
|
+
export function imMessage(m) {
|
|
267
|
+
return n.message({
|
|
268
|
+
id: m.server_message_id,
|
|
269
|
+
conversation_id: m.conversation_id,
|
|
270
|
+
from: n.userRef({ id: m.sender }),
|
|
271
|
+
type: 'text',
|
|
272
|
+
text: m.text,
|
|
273
|
+
created_at: n.time(m.create_time),
|
|
274
|
+
}, m);
|
|
275
|
+
}
|
|
276
|
+
export function conversation(id, peer, last, raw) {
|
|
277
|
+
return n.conversation({
|
|
278
|
+
id,
|
|
279
|
+
peer: peer ? n.userRef({ id: peer }) : null,
|
|
280
|
+
last_message: last?.text ?? null,
|
|
281
|
+
updated_at: last ? n.time(last.create_time) : null,
|
|
282
|
+
}, raw);
|
|
283
|
+
}
|
|
284
|
+
export function poi(v) {
|
|
285
|
+
return n.poi({ id: n.id(v.poi_id ?? v.id), name: String(v.poi_name ?? v.name ?? ''), address: n.str(v.address ?? v.poi_address ?? v.city_name) }, v);
|
|
286
|
+
}
|