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,235 @@
|
|
|
1
|
+
import * as n from '../../../core/normalize.js';
|
|
2
|
+
import { X_HOST } from './profile.js';
|
|
3
|
+
/** X 原始对象 → 归一化类型(AGENTS 6.2),字段取值参照上游 utils/data_util.py 的 handle_work_info。 */
|
|
4
|
+
/** TweetWithVisibilityResults 包了一层 tweet,统一拆开(上游 handle_work_info 开头)。 */
|
|
5
|
+
export function unwrap(result) {
|
|
6
|
+
if (result?.__typename === 'TweetWithVisibilityResults')
|
|
7
|
+
return result.tweet ?? result;
|
|
8
|
+
return result;
|
|
9
|
+
}
|
|
10
|
+
function screenNameOf(u) {
|
|
11
|
+
return n.str(u?.core?.screen_name ?? u?.legacy?.screen_name);
|
|
12
|
+
}
|
|
13
|
+
export const userUrl = (screenName) => `${X_HOST}/${screenName}`;
|
|
14
|
+
export const tweetUrl = (id, screenName) => (screenName ? `${X_HOST}/${screenName}/status/${id}` : `${X_HOST}/i/status/${id}`);
|
|
15
|
+
/** 用户对象(tweet 的 core.user_results.result、UserByScreenName 的 user.result、Viewer 的 user_results.result)。 */
|
|
16
|
+
export function userRef(u) {
|
|
17
|
+
if (!u)
|
|
18
|
+
return null;
|
|
19
|
+
const screenName = screenNameOf(u);
|
|
20
|
+
return n.userRef({ id: u.rest_id ?? u.legacy?.id_str, name: u.core?.name ?? u.legacy?.name, url: screenName ? userUrl(screenName) : null });
|
|
21
|
+
}
|
|
22
|
+
/** 完整用户资料:新版字段在 core / avatar / profile_bio / relationship_counts / tweet_counts,老版在 legacy。 */
|
|
23
|
+
export function user(u) {
|
|
24
|
+
const legacy = u.legacy ?? {};
|
|
25
|
+
const screenName = screenNameOf(u);
|
|
26
|
+
return n.user({
|
|
27
|
+
id: n.id(u.rest_id ?? legacy.id_str),
|
|
28
|
+
name: n.str(u.core?.name ?? legacy.name),
|
|
29
|
+
handle: screenName,
|
|
30
|
+
avatar: n.url(u.avatar?.image_url ?? legacy.profile_image_url_https),
|
|
31
|
+
url: screenName ? userUrl(screenName) : null,
|
|
32
|
+
bio: n.str(u.profile_bio?.description ?? legacy.description),
|
|
33
|
+
stats: {
|
|
34
|
+
followers: n.count(u.relationship_counts?.followers ?? legacy.followers_count),
|
|
35
|
+
following: n.count(u.relationship_counts?.following ?? legacy.friends_count),
|
|
36
|
+
items: n.count(u.tweet_counts?.tweets ?? legacy.statuses_count),
|
|
37
|
+
likes: n.count(u.action_counts?.favorites_count ?? legacy.favourites_count),
|
|
38
|
+
},
|
|
39
|
+
}, u);
|
|
40
|
+
}
|
|
41
|
+
/** 推文的媒体:`extended_entities` 才有完整列表,`entities` 只有第一张。视频取码率最高的 mp4。 */
|
|
42
|
+
function mediaOf(legacy) {
|
|
43
|
+
const list = (legacy.extended_entities ?? legacy.entities ?? {}).media ?? [];
|
|
44
|
+
const media = [];
|
|
45
|
+
for (const m of list) {
|
|
46
|
+
const info = m.original_info ?? {};
|
|
47
|
+
if (m.type === 'photo') {
|
|
48
|
+
media.push(n.media({ id: n.id(m.id_str), type: 'image', url: m.media_url_https, width: n.count(info.width), height: n.count(info.height) }, m));
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const mp4 = (m.video_info?.variants ?? []).filter((v) => v.content_type === 'video/mp4');
|
|
52
|
+
const best = mp4.sort((a, b) => (b.bitrate ?? 0) - (a.bitrate ?? 0))[0];
|
|
53
|
+
if (!best)
|
|
54
|
+
continue;
|
|
55
|
+
const ms = m.video_info?.duration_millis;
|
|
56
|
+
media.push(n.media({ id: n.id(m.id_str), type: 'video', url: best.url, width: n.count(info.width), height: n.count(info.height), duration: ms == null ? null : n.seconds(ms / 1000) }, m));
|
|
57
|
+
}
|
|
58
|
+
return { media, cover: n.url(list[0]?.media_url_https) };
|
|
59
|
+
}
|
|
60
|
+
/** 推文对象里的正文:长推文的全文在 note_tweet 里,legacy.full_text 是截断的。 */
|
|
61
|
+
function textOf(t) {
|
|
62
|
+
return n.str(t.note_tweet?.note_tweet_results?.result?.text ?? t.legacy?.full_text);
|
|
63
|
+
}
|
|
64
|
+
/** tweet result → Item。kind:有视频为 video,有图片为 image,否则 text(上游 work_type)。 */
|
|
65
|
+
export function item(raw) {
|
|
66
|
+
const t = unwrap(raw);
|
|
67
|
+
const legacy = t.legacy ?? {};
|
|
68
|
+
const author = t.core?.user_results?.result;
|
|
69
|
+
const id = n.id(t.rest_id ?? legacy.id_str);
|
|
70
|
+
const { media, cover } = mediaOf(legacy);
|
|
71
|
+
return n.item({
|
|
72
|
+
id,
|
|
73
|
+
kind: media.some((m) => m.type === 'video') ? 'video' : media.length ? 'image' : 'text',
|
|
74
|
+
url: tweetUrl(id, screenNameOf(author)),
|
|
75
|
+
text: textOf(t),
|
|
76
|
+
author: userRef(author),
|
|
77
|
+
created_at: n.time(legacy.created_at),
|
|
78
|
+
cover,
|
|
79
|
+
media,
|
|
80
|
+
stats: {
|
|
81
|
+
views: n.count(t.views?.count),
|
|
82
|
+
likes: n.count(legacy.favorite_count),
|
|
83
|
+
comments: n.count(legacy.reply_count),
|
|
84
|
+
collects: n.count(legacy.bookmark_count),
|
|
85
|
+
shares: n.count(legacy.retweet_count),
|
|
86
|
+
},
|
|
87
|
+
}, raw);
|
|
88
|
+
}
|
|
89
|
+
/** 回复推文 → Comment。直接回复本推文的 parent_id 为 null,楼中楼为被回复的那条。 */
|
|
90
|
+
export function comment(raw, itemId) {
|
|
91
|
+
const t = unwrap(raw);
|
|
92
|
+
const legacy = t.legacy ?? {};
|
|
93
|
+
const parent = n.idOrNull(legacy.in_reply_to_status_id_str);
|
|
94
|
+
return n.comment({
|
|
95
|
+
id: n.id(t.rest_id ?? legacy.id_str),
|
|
96
|
+
item_id: itemId,
|
|
97
|
+
parent_id: parent === itemId ? null : parent,
|
|
98
|
+
author: userRef(t.core?.user_results?.result),
|
|
99
|
+
text: textOf(t) ?? '',
|
|
100
|
+
created_at: n.time(legacy.created_at),
|
|
101
|
+
stats: { likes: n.count(legacy.favorite_count), replies: n.count(legacy.reply_count) },
|
|
102
|
+
}, raw);
|
|
103
|
+
}
|
|
104
|
+
// ---------------------------------------------------------------- timeline
|
|
105
|
+
/** 深度遍历 timeline,产出所有带 entryId + content 的 entry(上游 _walk_entries)。 */
|
|
106
|
+
function* walkEntries(node) {
|
|
107
|
+
if (Array.isArray(node)) {
|
|
108
|
+
for (const v of node)
|
|
109
|
+
yield* walkEntries(v);
|
|
110
|
+
}
|
|
111
|
+
else if (node && typeof node === 'object') {
|
|
112
|
+
if ('entryId' in node && 'content' in node)
|
|
113
|
+
yield node;
|
|
114
|
+
for (const v of Object.values(node))
|
|
115
|
+
yield* walkEntries(v);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** 真正的推文条目:跳过游标、广告、模块占位和已删除 / 不可见的推文(上游 extract_tweet_entries)。 */
|
|
119
|
+
export function tweetResults(res) {
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const entry of walkEntries(res)) {
|
|
122
|
+
if (!String(entry.entryId).startsWith('tweet-'))
|
|
123
|
+
continue;
|
|
124
|
+
const result = entry.content?.itemContent?.tweet_results?.result;
|
|
125
|
+
if (unwrap(result)?.rest_id)
|
|
126
|
+
out.push(result);
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 宫格模块里的推文(媒体搜索 product=Media):首页是 `search-grid-0` 模块的 items,翻页用 TimelineAddToModule
|
|
132
|
+
* 往模块里追加 moduleItems;模块条目只有 entryId + item,不是顶层 entry,tweetResults 取不到。
|
|
133
|
+
*/
|
|
134
|
+
export function gridResults(res) {
|
|
135
|
+
const out = [];
|
|
136
|
+
const walk = (node) => {
|
|
137
|
+
if (Array.isArray(node)) {
|
|
138
|
+
for (const v of node)
|
|
139
|
+
walk(v);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (!node || typeof node !== 'object')
|
|
143
|
+
return;
|
|
144
|
+
if (String(node.entryId ?? '').startsWith('search-grid-') && node.item) {
|
|
145
|
+
const result = node.item.itemContent?.tweet_results?.result;
|
|
146
|
+
if (unwrap(result)?.rest_id)
|
|
147
|
+
out.push(result);
|
|
148
|
+
}
|
|
149
|
+
for (const v of Object.values(node))
|
|
150
|
+
walk(v);
|
|
151
|
+
};
|
|
152
|
+
walk(res);
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
/** 搜索 People 的用户条目。 */
|
|
156
|
+
export function userResults(res) {
|
|
157
|
+
const out = [];
|
|
158
|
+
for (const entry of walkEntries(res)) {
|
|
159
|
+
if (!String(entry.entryId).startsWith('user-'))
|
|
160
|
+
continue;
|
|
161
|
+
const result = entry.content?.itemContent?.user_results?.result;
|
|
162
|
+
if (result?.rest_id)
|
|
163
|
+
out.push(result);
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
/** TweetDetail 里的回复:在 conversationthread-* 模块的 items 里,第一条是直接回复,其后是楼中楼。 */
|
|
168
|
+
export function replyResults(res) {
|
|
169
|
+
const out = [];
|
|
170
|
+
for (const entry of walkEntries(res)) {
|
|
171
|
+
if (!String(entry.entryId).startsWith('conversationthread-'))
|
|
172
|
+
continue;
|
|
173
|
+
for (const it of entry.content?.items ?? []) {
|
|
174
|
+
const result = it.item?.itemContent?.tweet_results?.result;
|
|
175
|
+
if (unwrap(result)?.rest_id)
|
|
176
|
+
out.push(result);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
/** 按 cursorType 在整棵树里找游标(上游 extract_cursor)。 */
|
|
182
|
+
export function cursorOf(res, direction = 'Bottom') {
|
|
183
|
+
for (const entry of walkEntries(res)) {
|
|
184
|
+
const content = entry.content ?? {};
|
|
185
|
+
for (const holder of [content, content.itemContent ?? {}]) {
|
|
186
|
+
if (holder && typeof holder === 'object' && holder.cursorType === direction && holder.value)
|
|
187
|
+
return holder.value;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
// ---------------------------------------------------------------- X Chat
|
|
193
|
+
/** 会话的其他参与者(自己以外)。participants_results 的条目是 `{rest_id, result?}`,result 才有资料。 */
|
|
194
|
+
function othersOf(entry, selfId) {
|
|
195
|
+
const participants = entry.conversation_detail?.participants_results ?? [];
|
|
196
|
+
return participants.map((p) => p.result ?? p).filter((u) => u?.rest_id && u.rest_id !== selfId);
|
|
197
|
+
}
|
|
198
|
+
/** 单聊里对方资料缺名字时,需要查成员资料的 rest_id(群聊不给 peer,不用查)。 */
|
|
199
|
+
export function peersWithoutName(entry, selfId) {
|
|
200
|
+
if (entry.conversation_detail?.group_metadata)
|
|
201
|
+
return [];
|
|
202
|
+
const peer = othersOf(entry, selfId)[0];
|
|
203
|
+
return peer && !userRef(peer)?.name ? [String(peer.rest_id)] : [];
|
|
204
|
+
}
|
|
205
|
+
/** GetUsersByIdsForXChat 的结果:rest_id → 用户对象(`get_member_results.results[].member_results.result`)。 */
|
|
206
|
+
export function memberResults(res) {
|
|
207
|
+
const out = new Map();
|
|
208
|
+
for (const r of res?.data?.get_member_results?.results ?? []) {
|
|
209
|
+
const u = r?.member_results?.result ?? r?.result;
|
|
210
|
+
if (u?.rest_id)
|
|
211
|
+
out.set(String(u.rest_id), u);
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* 收件箱条目 → Conversation。单聊的 peer 是自己以外的那个参与者,资料不全时用成员资料补;
|
|
217
|
+
* 消息是端到端加密的,不解。
|
|
218
|
+
*/
|
|
219
|
+
export function conversation(entry, selfId, members = new Map()) {
|
|
220
|
+
const detail = entry.conversation_detail ?? {};
|
|
221
|
+
const group = detail.group_metadata;
|
|
222
|
+
const peer = othersOf(entry, selfId)[0];
|
|
223
|
+
return n.conversation({
|
|
224
|
+
id: n.id(detail.conversation_id),
|
|
225
|
+
peer: group || !peer ? null : userRef(userRef(peer)?.name ? peer : (members.get(String(peer.rest_id)) ?? peer)),
|
|
226
|
+
updated_at: n.time(group?.updated_at_msec),
|
|
227
|
+
}, entry);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* X Chat 的消息事件是 base64 的加密载荷(端到端加密,要用户的 passcode 才能解),
|
|
231
|
+
* 这里只按顺序给出占位的 Message,原始事件用 `--raw` 取。
|
|
232
|
+
*/
|
|
233
|
+
export function encryptedMessage(encoded, index, conversationId) {
|
|
234
|
+
return n.message({ id: String(index), conversation_id: conversationId, type: 'other' }, encoded);
|
|
235
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 设备画像与请求头装配,移植自上游 utils/fingerprint.py + builder/client.py + builder/header.py。
|
|
3
|
+
*
|
|
4
|
+
* X 专属签名头(authorization / x-csrf-token / x-twitter-* / x-client-transaction-id / content-type)
|
|
5
|
+
* 由本文件按浏览器实抓顺序生成;浏览器通用头(user-agent / sec-ch-ua* / accept / sec-fetch-* / priority)
|
|
6
|
+
* 对应上游传输层 `_apply_fetch_headers` 追加在 X 头之后。specific-ai 关掉默认头,所以这里显式写全。
|
|
7
|
+
*/
|
|
8
|
+
// X web 公开固定 bearer(不是用户身份,所有 web 接口都带它,上游 PUBLIC_BEARER)。
|
|
9
|
+
export const PUBLIC_BEARER = 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
|
|
10
|
+
/** 当前真实 Chrome 153 画像(上游 get_profile)。所有请求的 UA / UA-CH 都取自这里,保持一致。 */
|
|
11
|
+
export const PROFILE = {
|
|
12
|
+
ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36',
|
|
13
|
+
secChUa: '"Google Chrome";v="153", "Not_A Brand";v="8", "Chromium";v="153"',
|
|
14
|
+
secChUaMobile: '?0',
|
|
15
|
+
secChUaPlatform: '"Windows"',
|
|
16
|
+
acceptLanguage: 'zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7,ja;q=0.6',
|
|
17
|
+
clientLanguage: 'zh-CN',
|
|
18
|
+
timezone: 'Asia/Shanghai',
|
|
19
|
+
};
|
|
20
|
+
// wreq-js 最高到 chrome_149;应用层 UA/UA-CH 显式覆盖为 Chrome 153(与上游同思路)。
|
|
21
|
+
export const BROWSER = 'chrome_149';
|
|
22
|
+
export const X_HOST = 'https://x.com';
|
|
23
|
+
export const API_X = 'https://api.x.com';
|
|
24
|
+
export const UPLOAD_HOST = 'https://upload.x.com';
|
|
25
|
+
export const COOKIE_DOMAIN = '.x.com';
|
|
26
|
+
/** Sec-Fetch-Site:按目标 host 相对 x.com 的关系(上游 _sec_fetch_site)。 */
|
|
27
|
+
function secFetchSite(url) {
|
|
28
|
+
const host = (new URL(url).hostname || '').toLowerCase();
|
|
29
|
+
if (host === 'x.com' || host === 'www.x.com')
|
|
30
|
+
return 'same-origin';
|
|
31
|
+
if (host.endsWith('.x.com') || host === 'twitter.com' || host.endsWith('.twitter.com'))
|
|
32
|
+
return 'same-site';
|
|
33
|
+
return 'cross-site';
|
|
34
|
+
}
|
|
35
|
+
/** 真实浏览器 fetch/XHR 的通用头(上游 _apply_fetch_headers),追加在 X 头之后。 */
|
|
36
|
+
export function fetchHeaders(url) {
|
|
37
|
+
return [
|
|
38
|
+
['user-agent', PROFILE.ua],
|
|
39
|
+
['sec-ch-ua', PROFILE.secChUa],
|
|
40
|
+
['sec-ch-ua-mobile', PROFILE.secChUaMobile],
|
|
41
|
+
['sec-ch-ua-platform', PROFILE.secChUaPlatform],
|
|
42
|
+
['accept', '*/*'],
|
|
43
|
+
['accept-language', PROFILE.acceptLanguage],
|
|
44
|
+
['priority', 'u=1, i'],
|
|
45
|
+
['sec-fetch-site', secFetchSite(url)],
|
|
46
|
+
['sec-fetch-mode', 'cors'],
|
|
47
|
+
['sec-fetch-dest', 'empty'],
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 装配一次请求的完整头,顺序逐项对齐浏览器实抓(2026-08 上游 builder/header.py 三族顺序)。
|
|
52
|
+
* `url` 用来算 sec-fetch-site 并追加 fetch 头。
|
|
53
|
+
*/
|
|
54
|
+
export function buildHeaders(type, url, auth, options = {}) {
|
|
55
|
+
const x = [];
|
|
56
|
+
const authType = () => {
|
|
57
|
+
if (auth.loggedIn)
|
|
58
|
+
x.push(['x-twitter-auth-type', 'OAuth2Session']);
|
|
59
|
+
};
|
|
60
|
+
if (type === 'GRAPHQL') {
|
|
61
|
+
x.push(['content-type', 'application/json']);
|
|
62
|
+
x.push(['authorization', auth.bearer]);
|
|
63
|
+
authType();
|
|
64
|
+
x.push(['x-csrf-token', auth.ct0]);
|
|
65
|
+
x.push(['x-twitter-client-language', auth.lang]);
|
|
66
|
+
x.push(['x-twitter-active-user', 'yes']);
|
|
67
|
+
if (options.xctid)
|
|
68
|
+
x.push(['x-client-transaction-id', options.xctid]);
|
|
69
|
+
if (options.origin)
|
|
70
|
+
x.push(['origin', options.origin]);
|
|
71
|
+
if (options.referer)
|
|
72
|
+
x.push(['referer', options.referer]);
|
|
73
|
+
}
|
|
74
|
+
else if (type === 'FORM') {
|
|
75
|
+
x.push(['authorization', auth.bearer]);
|
|
76
|
+
authType();
|
|
77
|
+
x.push(['x-csrf-token', auth.ct0]);
|
|
78
|
+
x.push(['x-twitter-client-language', auth.lang]);
|
|
79
|
+
x.push(['x-twitter-active-user', 'yes']);
|
|
80
|
+
x.push(['content-type', 'application/x-www-form-urlencoded; charset=UTF-8']);
|
|
81
|
+
if (options.xctid)
|
|
82
|
+
x.push(['x-client-transaction-id', options.xctid]);
|
|
83
|
+
if (options.origin)
|
|
84
|
+
x.push(['origin', options.origin]);
|
|
85
|
+
if (options.referer)
|
|
86
|
+
x.push(['referer', options.referer]);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// UPLOAD:只有这几个头,无 xctid / client-language / active-user(上游 _UPLOAD_ORDER)。
|
|
90
|
+
x.push(['authorization', auth.bearer]);
|
|
91
|
+
authType();
|
|
92
|
+
if (options.referer)
|
|
93
|
+
x.push(['referer', options.referer]);
|
|
94
|
+
x.push(['x-csrf-token', auth.ct0]);
|
|
95
|
+
if (options.origin)
|
|
96
|
+
x.push(['origin', options.origin]);
|
|
97
|
+
}
|
|
98
|
+
return [...x, ...fetchHeaders(url)];
|
|
99
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { SpecificAIError } from '../../../core/errors.js';
|
|
2
|
+
/** 参数归一化(AGENTS 4.8),移植自上游 utils/x_util.py。 */
|
|
3
|
+
const STATUS_RE = /(?:twitter|x)\.com\/(?:#!\/)?(\w+)\/status(?:es)?\/(\d+)/;
|
|
4
|
+
const SCREEN_NAME_RE = /(?:twitter|x)\.com\/@?([A-Za-z0-9_]{1,15})\/?(?:\?|$)/;
|
|
5
|
+
/** 接受完整推文链接或纯数字 id,统一返回数字 id(上游 parse_tweet_id)。 */
|
|
6
|
+
export function parseTweetId(input) {
|
|
7
|
+
const text = (input ?? '').trim();
|
|
8
|
+
if (/^\d+$/.test(text))
|
|
9
|
+
return text;
|
|
10
|
+
const m = STATUS_RE.exec(text);
|
|
11
|
+
if (!m)
|
|
12
|
+
throw new SpecificAIError('USAGE', `无法从 ${input} 解析推文 id`, { hint: '传推文 id 或链接 https://x.com/<user>/status/<id>' });
|
|
13
|
+
return m[2];
|
|
14
|
+
}
|
|
15
|
+
const ARTICLE_RE = /(?:twitter|x)\.com\/(?:i\/article|compose\/articles\/edit)\/(\d+)/;
|
|
16
|
+
/** 文章:纯数字 id、文章链接 `x.com/i/article/<id>` 或编辑页 `x.com/compose/articles/edit/<id>`。 */
|
|
17
|
+
export function parseArticleId(input) {
|
|
18
|
+
const text = (input ?? '').trim();
|
|
19
|
+
if (/^\d+$/.test(text))
|
|
20
|
+
return text;
|
|
21
|
+
const m = ARTICLE_RE.exec(text);
|
|
22
|
+
if (!m)
|
|
23
|
+
throw new SpecificAIError('USAGE', `无法从 ${input} 解析文章 id`, { hint: '传文章 id 或链接 https://x.com/i/article/<id>' });
|
|
24
|
+
return m[1];
|
|
25
|
+
}
|
|
26
|
+
/** 接受主页链接、@handle 或裸用户名,统一返回小写 screen_name(上游 parse_screen_name)。 */
|
|
27
|
+
export function parseScreenName(input) {
|
|
28
|
+
const text = (input ?? '').trim().replace(/\/+$/, '');
|
|
29
|
+
const m = SCREEN_NAME_RE.exec(text);
|
|
30
|
+
if (m)
|
|
31
|
+
return m[1].toLowerCase();
|
|
32
|
+
return text.replace(/^@+/, '').split('/').pop().split('?')[0].toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 用户参数里的纯数字按用户 ID(rest_id)处理,这样输出里的 `id` 可以直接作为下一条命令的参数;
|
|
36
|
+
* 全数字的用户名请写成 `@123` 或主页链接。
|
|
37
|
+
*/
|
|
38
|
+
export function isUserId(input) {
|
|
39
|
+
return /^\d+$/.test((input ?? '').trim());
|
|
40
|
+
}
|
|
41
|
+
/** twid cookie 形如 `u%3D1718802931036622848`,取出数字用户 id。 */
|
|
42
|
+
export function userIdFromTwid(twid) {
|
|
43
|
+
return (twid ?? '').replace('u%3D', '').replace('u=', '').replace(/"/g, '').trim();
|
|
44
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { staticFile } from '../../../core/paths.js';
|
|
4
|
+
import * as rand from '../../../core/rand.js';
|
|
5
|
+
/**
|
|
6
|
+
* x-client-transaction-id(XCTID)纯算实现,移植自上游 utils/transaction.py。
|
|
7
|
+
*
|
|
8
|
+
* X 前端对**每个** API 请求现算一个一次性签名头。素材(key / 4 帧动画路径 / 4 个下标)
|
|
9
|
+
* 来自登录页 app shell,一次会话内稳定;specific-ai 直接读打包进仓库的
|
|
10
|
+
* `static/x/transaction_l1.json`(对应上游 `ClientTransaction.from_cached`)。
|
|
11
|
+
*
|
|
12
|
+
* 算法:由素材算出稳定的 animation_key,再对 `method!path!t{KEYWORD}{anim_key}` 取 sha256,
|
|
13
|
+
* 拼上 key_bytes + t 的 4 字节小端 + digest[:16] + [3],最后混入一个随机字节做 base64(去 =)。
|
|
14
|
+
* 因为末尾混入随机字节,同输入不同输出;固定随机源时可逐字节对拍。
|
|
15
|
+
*/
|
|
16
|
+
const EPOCH_SECONDS = 1682924400;
|
|
17
|
+
const DEFAULT_KEYWORD = 'obfiowerehiring';
|
|
18
|
+
const ADDITIONAL_RANDOM_NUMBER = 3;
|
|
19
|
+
const TOTAL_TIME = 4096;
|
|
20
|
+
let cached = null;
|
|
21
|
+
/** 打包进仓库的 XCTID 素材(对拍与离线都用它,不发网络)。 */
|
|
22
|
+
export function transaction() {
|
|
23
|
+
if (!cached) {
|
|
24
|
+
const profile = JSON.parse(readFileSync(staticFile('x', 'transaction_l1.json'), 'utf8'));
|
|
25
|
+
cached = new ClientTransaction(profile.key, profile.frames, profile.indices);
|
|
26
|
+
}
|
|
27
|
+
return cached;
|
|
28
|
+
}
|
|
29
|
+
function bezier(a, b, m) {
|
|
30
|
+
return 3.0 * a * (1 - m) * (1 - m) * m + 3.0 * b * (1 - m) * m * m + m * m * m;
|
|
31
|
+
}
|
|
32
|
+
/** cubic-bezier(c0, c1, c2, c3) 求值,逐行对齐上游 Cubic.get_value。 */
|
|
33
|
+
function cubicValue(c, target) {
|
|
34
|
+
let startGradient = 0;
|
|
35
|
+
let endGradient = 0;
|
|
36
|
+
let start = 0;
|
|
37
|
+
let end = 1;
|
|
38
|
+
let mid = 0;
|
|
39
|
+
if (target <= 0.0) {
|
|
40
|
+
if (c[0] > 0.0)
|
|
41
|
+
startGradient = c[1] / c[0];
|
|
42
|
+
else if (!c[1] && c[2] > 0.0)
|
|
43
|
+
startGradient = c[3] / c[2];
|
|
44
|
+
return startGradient * target;
|
|
45
|
+
}
|
|
46
|
+
if (target >= 1.0) {
|
|
47
|
+
if (c[2] < 1.0)
|
|
48
|
+
endGradient = (c[3] - 1.0) / (c[2] - 1.0);
|
|
49
|
+
else if (c[2] === 1.0 && c[0] < 1.0)
|
|
50
|
+
endGradient = (c[1] - 1.0) / (c[0] - 1.0);
|
|
51
|
+
return 1.0 + endGradient * (target - 1.0);
|
|
52
|
+
}
|
|
53
|
+
while (start < end) {
|
|
54
|
+
mid = (start + end) / 2;
|
|
55
|
+
const estimate = bezier(c[0], c[2], mid);
|
|
56
|
+
if (Math.abs(target - estimate) < 0.00001)
|
|
57
|
+
return bezier(c[1], c[3], mid);
|
|
58
|
+
if (estimate < target)
|
|
59
|
+
start = mid;
|
|
60
|
+
else
|
|
61
|
+
end = mid;
|
|
62
|
+
}
|
|
63
|
+
return bezier(c[1], c[3], mid);
|
|
64
|
+
}
|
|
65
|
+
function interpolate(from, to, f) {
|
|
66
|
+
return from.map((a, i) => a * (1 - f) + to[i] * f);
|
|
67
|
+
}
|
|
68
|
+
function rotationMatrix(degrees) {
|
|
69
|
+
const rad = (degrees * Math.PI) / 180;
|
|
70
|
+
return [Math.cos(rad), -Math.sin(rad), Math.sin(rad), Math.cos(rad)];
|
|
71
|
+
}
|
|
72
|
+
/** 复刻上游 _float_to_hex:整数部分逐位取余,小数部分逐位展开。 */
|
|
73
|
+
function floatToHex(x) {
|
|
74
|
+
const result = [];
|
|
75
|
+
let quotient = Math.trunc(x);
|
|
76
|
+
let fraction = x - quotient;
|
|
77
|
+
while (quotient > 0) {
|
|
78
|
+
quotient = Math.trunc(x / 16);
|
|
79
|
+
const remainder = Math.trunc(x - quotient * 16);
|
|
80
|
+
result.unshift(remainder > 9 ? String.fromCharCode(remainder + 55) : String(remainder));
|
|
81
|
+
x = quotient;
|
|
82
|
+
}
|
|
83
|
+
if (fraction === 0)
|
|
84
|
+
return result.join('');
|
|
85
|
+
result.push('.');
|
|
86
|
+
while (fraction > 0) {
|
|
87
|
+
fraction *= 16;
|
|
88
|
+
const integer = Math.trunc(fraction);
|
|
89
|
+
fraction -= integer;
|
|
90
|
+
result.push(integer > 9 ? String.fromCharCode(integer + 55) : String(integer));
|
|
91
|
+
}
|
|
92
|
+
return result.join('');
|
|
93
|
+
}
|
|
94
|
+
function solve(value, minVal, maxVal, rounding) {
|
|
95
|
+
const result = (value * (maxVal - minVal)) / 255 + minVal;
|
|
96
|
+
return rounding ? Math.floor(result) : Math.round(result * 100) / 100;
|
|
97
|
+
}
|
|
98
|
+
function isOdd(num) {
|
|
99
|
+
return num % 2 ? -1.0 : 0.0;
|
|
100
|
+
}
|
|
101
|
+
export class ClientTransaction {
|
|
102
|
+
key;
|
|
103
|
+
frames;
|
|
104
|
+
indices;
|
|
105
|
+
keyBytes;
|
|
106
|
+
animationKey;
|
|
107
|
+
constructor(key, frames, indices) {
|
|
108
|
+
this.key = key;
|
|
109
|
+
this.frames = frames;
|
|
110
|
+
this.indices = indices;
|
|
111
|
+
this.keyBytes = [...Buffer.from(key, 'base64')];
|
|
112
|
+
this.animationKey = this.buildAnimationKey();
|
|
113
|
+
}
|
|
114
|
+
/** 选中的帧路径按 'C' 切成若干行数字(上游 _frame_rows)。 */
|
|
115
|
+
frameRows() {
|
|
116
|
+
const frame = this.frames[this.keyBytes[5] % 4];
|
|
117
|
+
return frame
|
|
118
|
+
.slice(9)
|
|
119
|
+
.split('C')
|
|
120
|
+
.map((segment) => segment
|
|
121
|
+
.replace(/[^\d]+/g, ' ')
|
|
122
|
+
.trim()
|
|
123
|
+
.split(/\s+/)
|
|
124
|
+
.filter(Boolean)
|
|
125
|
+
.map((x) => parseInt(x, 10)));
|
|
126
|
+
}
|
|
127
|
+
buildAnimationKey() {
|
|
128
|
+
const rowIndex = this.keyBytes[this.indices[0]] % 16;
|
|
129
|
+
let frameTime = 1;
|
|
130
|
+
for (const index of this.indices.slice(1))
|
|
131
|
+
frameTime *= this.keyBytes[index] % 16;
|
|
132
|
+
const rows = this.frameRows();
|
|
133
|
+
return ClientTransaction.animate(rows[rowIndex], frameTime / TOTAL_TIME);
|
|
134
|
+
}
|
|
135
|
+
static animate(frameRow, targetTime) {
|
|
136
|
+
const fromColor = [...frameRow.slice(0, 3), 1].map(Number);
|
|
137
|
+
const toColor = [...frameRow.slice(3, 6), 1].map(Number);
|
|
138
|
+
const fromRotation = [0.0];
|
|
139
|
+
const toRotation = [solve(Number(frameRow[6]), 60.0, 360.0, true)];
|
|
140
|
+
const curves = frameRow.slice(7).map((v, i) => solve(Number(v), isOdd(i), 1.0, false));
|
|
141
|
+
const progress = cubicValue(curves, targetTime);
|
|
142
|
+
const color = interpolate(fromColor, toColor, progress).map((v) => Math.max(v, 0));
|
|
143
|
+
const rotation = interpolate(fromRotation, toRotation, progress);
|
|
144
|
+
const matrix = rotationMatrix(rotation[0]);
|
|
145
|
+
const parts = color.slice(0, -1).map((v) => Math.round(v).toString(16));
|
|
146
|
+
for (const value of matrix) {
|
|
147
|
+
const rounded = Math.abs(Math.round(value * 100) / 100);
|
|
148
|
+
const hexValue = floatToHex(rounded);
|
|
149
|
+
if (hexValue.startsWith('.'))
|
|
150
|
+
parts.push(`0${hexValue}`.toLowerCase());
|
|
151
|
+
else
|
|
152
|
+
parts.push(hexValue || '0');
|
|
153
|
+
}
|
|
154
|
+
parts.push('0', '0');
|
|
155
|
+
return parts.join('').replace(/[.-]/g, '');
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 算一个 XCTID。path 必须是不含 query 的 pathname。
|
|
159
|
+
* timeNow / randomByte 仅供对拍覆盖。
|
|
160
|
+
*/
|
|
161
|
+
generate(method, path, timeNow, randomByte) {
|
|
162
|
+
const t = timeNow ?? rand.nowSeconds() - EPOCH_SECONDS;
|
|
163
|
+
const timeBytes = [0, 1, 2, 3].map((i) => (t >>> (i * 8)) & 0xff);
|
|
164
|
+
const message = `${method}!${path}!${t}${DEFAULT_KEYWORD}${this.animationKey}`;
|
|
165
|
+
const digest = createHash('sha256').update(message).digest();
|
|
166
|
+
const payload = [...this.keyBytes, ...timeBytes, ...digest.subarray(0, 16), ADDITIONAL_RANDOM_NUMBER];
|
|
167
|
+
const rnd = randomByte ?? rand.randint(0, 255);
|
|
168
|
+
const out = Buffer.from([rnd, ...payload.map((b) => b ^ rnd)]);
|
|
169
|
+
return out.toString('base64').replace(/=+$/, '');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { CATEGORY, filter, PRODUCT, visibility } from '../../core/options.js';
|
|
3
|
+
import { definePlatform, handlers } from '../../core/registry.js';
|
|
4
|
+
/** 懒加载 web 端的 handler:只有执行到这条命令时才加载实现。 */
|
|
5
|
+
const { h, impl } = handlers(() => import('./web/commands.js'));
|
|
6
|
+
export default definePlatform({
|
|
7
|
+
id: 'xhs',
|
|
8
|
+
name: '小红书',
|
|
9
|
+
aliases: ['xiaohongshu', 'rednote'],
|
|
10
|
+
item: '笔记',
|
|
11
|
+
endpoints: {
|
|
12
|
+
web: {
|
|
13
|
+
login: { methods: ['qrcode', 'sms', 'cookie'], default: 'qrcode', scopes: ['creator'] },
|
|
14
|
+
commands: {
|
|
15
|
+
'auth login': impl('full', 'authLogin'),
|
|
16
|
+
'auth status': impl('full', 'authStatus'),
|
|
17
|
+
'user get': impl('full', 'userGet'),
|
|
18
|
+
'user search': impl('full', 'userSearch'),
|
|
19
|
+
'user items': impl('full', 'userItems'),
|
|
20
|
+
'user likes': impl('full', 'userLikes'),
|
|
21
|
+
'user collects': impl('full', 'userCollects'),
|
|
22
|
+
'user followers': 'none',
|
|
23
|
+
'user following': impl('partial', 'userFollowing', { note: '只支持 me' }),
|
|
24
|
+
'user follow': 'none',
|
|
25
|
+
'user unfollow': 'none',
|
|
26
|
+
'item get': impl('full', 'itemGet'),
|
|
27
|
+
'item search': impl('full', 'itemSearch', {
|
|
28
|
+
options: {
|
|
29
|
+
sort: filter.sort('general', 'latest', 'popular', 'comments', 'collects'),
|
|
30
|
+
type: filter.type('all', 'video', 'image'),
|
|
31
|
+
time: filter.time('all', 'day', 'week', 'half_year'),
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
'item related': 'none',
|
|
35
|
+
'item list': impl('full', 'itemList'),
|
|
36
|
+
'item media': impl('full', 'itemMedia'),
|
|
37
|
+
'item download': impl('full', 'itemDownload'),
|
|
38
|
+
'item like': 'none',
|
|
39
|
+
'item unlike': 'none',
|
|
40
|
+
'item collect': 'none',
|
|
41
|
+
'item uncollect': 'none',
|
|
42
|
+
'item publish': impl('full', 'itemPublish', {
|
|
43
|
+
supports: ['title', 'text', 'image', 'video', 'cover', 'tag', 'topic', 'poi', 'visibility', 'schedule'],
|
|
44
|
+
options: {
|
|
45
|
+
visibility: visibility('public', 'private'),
|
|
46
|
+
poiName: z.string().optional().describe('地点名称(按它搜地点);同时给 --poi 时取 id 相同的那个,否则取名称完全相同的'),
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
'item delete': 'none',
|
|
50
|
+
'product get': 'none',
|
|
51
|
+
'comment list': impl('partial', 'commentList', { note: '--product 规划中', options: { product: PRODUCT } }),
|
|
52
|
+
'comment replies': impl('full', 'commentReplies'),
|
|
53
|
+
'comment add': 'none',
|
|
54
|
+
'comment delete': 'none',
|
|
55
|
+
'comment like': 'none',
|
|
56
|
+
'comment unlike': 'none',
|
|
57
|
+
'feed list': impl('partial', 'feedList', { note: 'following 规划中', options: { kind: filter.kind('recommend', 'following'), category: CATEGORY } }),
|
|
58
|
+
'feed categories': impl('full', 'feedCategories'),
|
|
59
|
+
'live get': impl('full', 'liveGet'),
|
|
60
|
+
'live list': impl('full', 'liveList', { options: { category: CATEGORY } }),
|
|
61
|
+
'live search': 'none',
|
|
62
|
+
'live categories': impl('full', 'liveCategories'),
|
|
63
|
+
'live listen': impl('full', 'liveListen'),
|
|
64
|
+
'live history': 'none',
|
|
65
|
+
'live send': impl('partial', 'liveSend', { note: '--gift 规划中' }),
|
|
66
|
+
'live like': 'none',
|
|
67
|
+
'live rank': 'none',
|
|
68
|
+
'live gifts': impl('full', 'liveGifts'),
|
|
69
|
+
'live products': impl('full', 'liveProducts'),
|
|
70
|
+
'live media': 'none',
|
|
71
|
+
'live start': 'none',
|
|
72
|
+
'live stop': 'none',
|
|
73
|
+
'keyword suggest': impl('full', 'keywordSuggest'),
|
|
74
|
+
'keyword hot': impl('full', 'keywordHot'),
|
|
75
|
+
'notice list': impl('full', 'noticeList'),
|
|
76
|
+
'notice count': impl('full', 'noticeCount'),
|
|
77
|
+
'msg list': impl('full', 'msgList', { note: '含群聊,群聊会话 id 为 group:<群 id>' }),
|
|
78
|
+
'msg history': impl('full', 'msgHistory', { note: '单聊传对方 id,群聊传 group:<群 id>' }),
|
|
79
|
+
'msg send': impl('full', 'msgSend'),
|
|
80
|
+
'msg listen': impl('full', 'msgListen'),
|
|
81
|
+
'msg read': impl('full', 'msgRead'),
|
|
82
|
+
'msg revoke': impl('full', 'msgRevoke'),
|
|
83
|
+
'msg delete': impl('full', 'msgDelete'),
|
|
84
|
+
'media upload': impl('full', 'mediaUpload'),
|
|
85
|
+
'folder list': impl('partial', 'folderList', { note: '只列公开的收藏夹(上游只有这一个接口),收藏夹里的笔记上游没有' }),
|
|
86
|
+
// 上游只有收藏夹列表,没有收藏夹内容
|
|
87
|
+
'folder items': 'none',
|
|
88
|
+
'folder create': 'none',
|
|
89
|
+
'folder update': 'none',
|
|
90
|
+
'folder delete': 'none',
|
|
91
|
+
'series list': 'none',
|
|
92
|
+
'series items': 'none',
|
|
93
|
+
'history list': 'none',
|
|
94
|
+
'topic search': impl('full', 'topicSearch'),
|
|
95
|
+
'poi search': impl('full', 'poiSearch'),
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
app: 'planned',
|
|
99
|
+
pc: 'planned',
|
|
100
|
+
},
|
|
101
|
+
});
|