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,96 @@
|
|
|
1
|
+
import { SpecificAIError } from '../../../core/errors.js';
|
|
2
|
+
import { parseJson, parseJsonp } from '../../../core/http.js';
|
|
3
|
+
import * as rand from '../../../core/rand.js';
|
|
4
|
+
import { authError } from '../../../core/toolkit.js';
|
|
5
|
+
import { APP_KEY, DOC_HEADERS, IM_APP_KEY, LOGIN_TOKEN_URL, TOKEN_HEADERS, UA, UPLOAD_HEADERS, UPLOAD_URL } from './profile.js';
|
|
6
|
+
import { generateSign } from './sign.js';
|
|
7
|
+
/**
|
|
8
|
+
* 上游 taobao_apis.py 的 TaobaoApis:一个函数对应一个上游方法,参数与顺序照抄(对拍测试逐字节比较)。
|
|
9
|
+
*/
|
|
10
|
+
/** `_m_h5_tk` 过期或缺失时,服务端在响应里下发新的,重试即可。 */
|
|
11
|
+
const TOKEN_RETRY = /令牌过期|令牌为空|FAIL_SYS_TOKEN_EXOIRED|FAIL_SYS_TOKEN_EMPTY/;
|
|
12
|
+
const MAX_TOKEN_RETRY = 3;
|
|
13
|
+
/**
|
|
14
|
+
* get_token:`mtop.taobao.login.token.get.h5`,换取私信长连的 accessToken。
|
|
15
|
+
* 返回 JSONP 里的整个 JSON。签名用 cookie `_m_h5_tk`;没有这个 cookie(游客、导入的 cookie 不全)
|
|
16
|
+
* 时用空 token 签名,服务端会下发新的 `_m_h5_tk`,与令牌过期一样重试。
|
|
17
|
+
*/
|
|
18
|
+
export async function getToken(tb) {
|
|
19
|
+
for (let attempt = 1;; attempt++) {
|
|
20
|
+
const t = rand.nowSeconds() * 1000;
|
|
21
|
+
const data = `{"domain":"cntaobao","deviceId":"${tb.deviceId}","locale":"zh_CN","imAppKey":"${IM_APP_KEY}"}`;
|
|
22
|
+
const token = (tb.cookie('_m_h5_tk') ?? '').split('_')[0];
|
|
23
|
+
const query = [
|
|
24
|
+
['jsv', '2.7.0'],
|
|
25
|
+
['appKey', APP_KEY],
|
|
26
|
+
['t', t],
|
|
27
|
+
['sign', generateSign(t, token, data)],
|
|
28
|
+
['api', 'mtop.taobao.login.token.get.h5'],
|
|
29
|
+
['v', '2.0'],
|
|
30
|
+
['preventFallback', 'true'],
|
|
31
|
+
['type', 'jsonp'],
|
|
32
|
+
['dataType', 'jsonp'],
|
|
33
|
+
['callback', 'mtopjsonp3'],
|
|
34
|
+
['data', data],
|
|
35
|
+
];
|
|
36
|
+
const text = await tb.text({ url: LOGIN_TOKEN_URL, query, headers: TOKEN_HEADERS });
|
|
37
|
+
const body = parseJsonp(text);
|
|
38
|
+
if (body == null)
|
|
39
|
+
throw new SpecificAIError('UPSTREAM', '淘宝返回的不是 mtop 的 JSONP', { detail: { body: text.slice(0, 300) } });
|
|
40
|
+
if (attempt < MAX_TOKEN_RETRY && TOKEN_RETRY.test(String(body?.ret?.[0] ?? '')))
|
|
41
|
+
continue;
|
|
42
|
+
return body;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** get_goods_uid_encrypt_uid 的第一步:打开商品页。 */
|
|
46
|
+
export async function goodsPage(tb, goodsUrl) {
|
|
47
|
+
const res = await tb.request({ url: goodsUrl, headers: DOC_HEADERS });
|
|
48
|
+
return { url: res.url || goodsUrl, html: await res.text() };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* get_goods_uid_encrypt_uid 的第二步:从商品页 HTML 里取卖家的 `uid` 与 `encrypt_uid`。
|
|
52
|
+
* 取不到时区分登录墙(游客打开商品页会被要求登录)、滑块验证和页面结构变化。
|
|
53
|
+
*/
|
|
54
|
+
export function parseSeller(tb, page) {
|
|
55
|
+
const uid = /"userId":"(.*?)"/.exec(page.html)?.[1];
|
|
56
|
+
const encrypt = /data-encryptuid="(.*?)"/.exec(page.html)?.[1];
|
|
57
|
+
if (uid != null && encrypt != null)
|
|
58
|
+
return { uid, encrypt_uid: encrypt };
|
|
59
|
+
if (/login_jump|login\.taobao\.com\/member\/login|login\.m\.taobao\.com/.test(page.html) || /\/\/login\.(m\.)?taobao\.com\//.test(page.url)) {
|
|
60
|
+
throw authError(tb.ctx, '淘宝要求登录后才能打开商品页');
|
|
61
|
+
}
|
|
62
|
+
if (/_____tmd_____|punish|x5sec/i.test(page.html)) {
|
|
63
|
+
throw new SpecificAIError('RISK_CONTROL', '淘宝要求滑块验证,请稍后再试或在浏览器里打开一次商品页', { detail: { kind: 'captcha' } });
|
|
64
|
+
}
|
|
65
|
+
throw new SpecificAIError('UPSTREAM', '商品页里没有找到卖家信息', { detail: { url: page.url } });
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* upload_media:上传图片,返回响应里的 `object`(fileId、url、size、pix)。
|
|
69
|
+
* 上游不管文件是什么格式,Content-Type 一律写 image/png,这里照做。
|
|
70
|
+
*/
|
|
71
|
+
export async function uploadMedia(tb, file) {
|
|
72
|
+
const res = await tb.request({
|
|
73
|
+
method: 'POST',
|
|
74
|
+
url: UPLOAD_URL,
|
|
75
|
+
query: [
|
|
76
|
+
['appkey', 'ampmedia'],
|
|
77
|
+
['folderId', '0'],
|
|
78
|
+
['_input_charset', 'utf-8'],
|
|
79
|
+
['useGtrSessionFilter', 'false'],
|
|
80
|
+
],
|
|
81
|
+
headers: UPLOAD_HEADERS,
|
|
82
|
+
multipart: [
|
|
83
|
+
{ name: 'name', data: file.filename },
|
|
84
|
+
{ name: 'ua', data: UA },
|
|
85
|
+
{ name: 'file', data: file.data, filename: file.filename, contentType: 'image/png' },
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
const body = await parseJson(res);
|
|
89
|
+
if (body?.success && body.object)
|
|
90
|
+
return body;
|
|
91
|
+
if (/NOT_LOGIN|SESSION/i.test(String(body?.errorCode ?? '')))
|
|
92
|
+
throw authError(tb.ctx, `淘宝登录态无效:${body.message ?? body.errorCode}`);
|
|
93
|
+
throw new SpecificAIError('UPSTREAM', `上传失败:${body?.message ?? body?.errorCode ?? '未知错误'}`, {
|
|
94
|
+
detail: { code: body?.errorCode ?? null, message: body?.message ?? null },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { SpecificAIError } from '../../../core/errors.js';
|
|
2
|
+
import { unquote } from '../../../core/py.js';
|
|
3
|
+
import * as rand from '../../../core/rand.js';
|
|
4
|
+
import { authError, httpClient } from '../../../core/toolkit.js';
|
|
5
|
+
import * as api from './api.js';
|
|
6
|
+
import { BROWSER } from './profile.js';
|
|
7
|
+
import { generateDeviceId } from './sign.js';
|
|
8
|
+
/**
|
|
9
|
+
* 淘宝 web 端的会话(上游 TaobaoApis + taobaoLive 的构造部分):cookie、设备号、当前账号。
|
|
10
|
+
*
|
|
11
|
+
* 上游用 requests 的 Session,cookie 是从字符串导入的(没有 domain),会发给所有域名,
|
|
12
|
+
* 顺序是 `session.cookies.get_dict()` 的顺序,即写入顺序。这里照做。
|
|
13
|
+
*/
|
|
14
|
+
export class Taobao {
|
|
15
|
+
ctx;
|
|
16
|
+
http;
|
|
17
|
+
jar;
|
|
18
|
+
constructor(ctx) {
|
|
19
|
+
this.ctx = ctx;
|
|
20
|
+
this.http = httpClient(ctx, { browser: BROWSER, os: 'windows' });
|
|
21
|
+
this.jar = this.http.jar;
|
|
22
|
+
}
|
|
23
|
+
/** 所有未过期的 cookie,按写入顺序;同名(不同 domain)时位置取第一个、值取最后一个,与 get_dict 相同。 */
|
|
24
|
+
cookies() {
|
|
25
|
+
const now = rand.nowSeconds();
|
|
26
|
+
const map = new Map();
|
|
27
|
+
for (const c of this.jar.cookies)
|
|
28
|
+
if (c.expires == null || c.expires > now)
|
|
29
|
+
map.set(c.name, c.value);
|
|
30
|
+
return Object.fromEntries(map);
|
|
31
|
+
}
|
|
32
|
+
/** 上游 get_session_cookies_str:`k=v; k=v`。 */
|
|
33
|
+
cookieString() {
|
|
34
|
+
return Object.entries(this.cookies())
|
|
35
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
36
|
+
.join('; ');
|
|
37
|
+
}
|
|
38
|
+
cookie(name) {
|
|
39
|
+
return this.cookies()[name];
|
|
40
|
+
}
|
|
41
|
+
/** 当前账号的用户 ID(cookie `unb`)。 */
|
|
42
|
+
get myId() {
|
|
43
|
+
return this.cookie('unb') ?? '';
|
|
44
|
+
}
|
|
45
|
+
/** cookie `_nk_` 的原值(没有时用 `tracknick`):URL 编码后的 `\uXXXX` 转义。上游私信的 sender_nick 直接拼它,不解码。 */
|
|
46
|
+
get rawNick() {
|
|
47
|
+
return this.cookie('_nk_') ?? this.cookie('tracknick') ?? '';
|
|
48
|
+
}
|
|
49
|
+
/** 当前账号的昵称(解码后的 `_nk_`)。 */
|
|
50
|
+
get nick() {
|
|
51
|
+
return decodeNick(this.rawNick);
|
|
52
|
+
}
|
|
53
|
+
/** 设备号:上游每次启动随机生成;这里生成一次后存进凭证的 device,之后复用。 */
|
|
54
|
+
get deviceId() {
|
|
55
|
+
const device = this.ctx.credential.device;
|
|
56
|
+
if (typeof device.device_id !== 'string' || !device.device_id)
|
|
57
|
+
device.device_id = generateDeviceId(this.myId);
|
|
58
|
+
return device.device_id;
|
|
59
|
+
}
|
|
60
|
+
me() {
|
|
61
|
+
return { id: this.myId, name: this.nick || null, url: null };
|
|
62
|
+
}
|
|
63
|
+
/** 私信、上传等需要登录的操作:cookie 里必须有 unb。 */
|
|
64
|
+
requireLogin() {
|
|
65
|
+
if (!this.myId)
|
|
66
|
+
throw authError(this.ctx, '当前账号的 cookie 里没有 unb,请重新登录');
|
|
67
|
+
}
|
|
68
|
+
request(req) {
|
|
69
|
+
return this.http.request({ ...req, cookies: this.cookies() });
|
|
70
|
+
}
|
|
71
|
+
async text(req) {
|
|
72
|
+
return (await this.request(req)).text();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function decodeNick(raw) {
|
|
76
|
+
return unquote(raw).replace(/\\u([0-9a-fA-F]{4})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
|
|
77
|
+
}
|
|
78
|
+
/** mtop 的 ret(如 `FAIL_SYS_SESSION_EXPIRED::Session过期`)映射成 specific-ai 的错误(AGENTS 6.4)。 */
|
|
79
|
+
function mtopError(ctx, body) {
|
|
80
|
+
const ret = String(body?.ret?.[0] ?? '');
|
|
81
|
+
if (/SESSION_EXPIRED|FAIL_SYS_SESSION|NOT_LOGIN|ERR_SID_INVALID/i.test(ret))
|
|
82
|
+
return authError(ctx, `淘宝登录态无效:${ret}`);
|
|
83
|
+
if (/FAIL_SYS_USER_VALIDATE/.test(ret)) {
|
|
84
|
+
return new SpecificAIError('RISK_CONTROL', `淘宝要求人机验证:${ret}`, { detail: { kind: 'captcha', ret: body?.ret } });
|
|
85
|
+
}
|
|
86
|
+
if (/RGV587|FAIL_SYS_TRAFFIC_LIMIT|FAIL_SYS_ILLEGAL_ACCESS/.test(ret)) {
|
|
87
|
+
return new SpecificAIError('RISK_CONTROL', `淘宝风控拦截:${ret}`, { detail: { kind: 'blocked', ret: body?.ret } });
|
|
88
|
+
}
|
|
89
|
+
return new SpecificAIError('UPSTREAM', ret ? `淘宝返回错误:${ret}` : '淘宝返回了无法识别的结果', { detail: { ret: body?.ret ?? null } });
|
|
90
|
+
}
|
|
91
|
+
/** 私信长连的 accessToken(上游 init 里 get_token 的用法)。 */
|
|
92
|
+
export async function accessToken(tb) {
|
|
93
|
+
const body = await api.getToken(tb);
|
|
94
|
+
const token = body?.data?.result?.accessToken;
|
|
95
|
+
if (typeof token === 'string' && token)
|
|
96
|
+
return token;
|
|
97
|
+
throw mtopError(tb.ctx, body);
|
|
98
|
+
}
|
|
99
|
+
/** 建立会话。 */
|
|
100
|
+
export function taobao(ctx) {
|
|
101
|
+
return new Taobao(ctx);
|
|
102
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { SpecificAIError } from '../../../core/errors.js';
|
|
2
|
+
import { readMedia } from '../../../core/files.js';
|
|
3
|
+
import { cookieCredential, finishLogin, loginContext } from '../../../core/login.js';
|
|
4
|
+
import * as n from '../../../core/normalize.js';
|
|
5
|
+
import * as rand from '../../../core/rand.js';
|
|
6
|
+
import { history, listen, pushedPayloads } from '../../_shared/impaas.js';
|
|
7
|
+
import * as api from './api.js';
|
|
8
|
+
import { accessToken, Taobao, taobao } from './client.js';
|
|
9
|
+
import { createChatFrame, createdCid, imId, openIm, sendMsgFrame } from './im.js';
|
|
10
|
+
import * as norm from './normalize.js';
|
|
11
|
+
import { COOKIE_DOMAIN, IM_DOMAIN } from './profile.js';
|
|
12
|
+
import { resolveConversation, resolveItem, resolveSellerItem } from './resolve.js';
|
|
13
|
+
import { decrypt } from './sign.js';
|
|
14
|
+
const isAuthError = (err) => err instanceof SpecificAIError && (err.code === 'AUTH_REQUIRED' || err.code === 'AUTH_EXPIRED');
|
|
15
|
+
// ================================================================ auth
|
|
16
|
+
/** cookie 登录:导入后换一次私信 token 作为校验(上游没有取当前用户的接口)。 */
|
|
17
|
+
export async function authLogin(ctx) {
|
|
18
|
+
const credential = cookieCredential(ctx, COOKIE_DOMAIN);
|
|
19
|
+
const tb = new Taobao(loginContext(ctx, credential));
|
|
20
|
+
const hint = '复制浏览器里登录后的淘宝 cookie(需要包含 unb、_m_h5_tk、cookie2)';
|
|
21
|
+
if (!tb.myId)
|
|
22
|
+
throw new SpecificAIError('USAGE', 'cookie 里没有 unb,不是登录后的淘宝 cookie', { hint });
|
|
23
|
+
try {
|
|
24
|
+
await accessToken(tb);
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
if (isAuthError(err))
|
|
28
|
+
throw new SpecificAIError('AUTH_REQUIRED', '登录没有成功:cookie 无效或已过期', { hint });
|
|
29
|
+
throw err;
|
|
30
|
+
}
|
|
31
|
+
return finishLogin(ctx, credential, tb.me());
|
|
32
|
+
}
|
|
33
|
+
export async function authStatus(ctx) {
|
|
34
|
+
const off = { logged_in: false, user: null, method: null, expires_at: null };
|
|
35
|
+
const tb = taobao(ctx);
|
|
36
|
+
// 没登录时是一份空凭证,没有 unb
|
|
37
|
+
if (!tb.myId)
|
|
38
|
+
return off;
|
|
39
|
+
try {
|
|
40
|
+
await accessToken(tb);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
if (isAuthError(err))
|
|
44
|
+
return off;
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
const expires = ['cookie2', 'sgcookie', 'unb'].map((name) => tb.jar.cookies.find((c) => c.name === name)?.expires).find((e) => e != null);
|
|
48
|
+
return { logged_in: true, user: tb.me(), method: ctx.credential.method, expires_at: n.time(expires) };
|
|
49
|
+
}
|
|
50
|
+
// ================================================================ user
|
|
51
|
+
/** 卖家资料:打开商品页取卖家(上游 get_goods_uid_encrypt_uid)。 */
|
|
52
|
+
export async function userGet(ctx) {
|
|
53
|
+
if (ctx.args.user === 'me') {
|
|
54
|
+
throw new SpecificAIError('NOT_IMPLEMENTED', 'taobao 的 user get me 尚未实现', { hint: 'specific-ai taobao auth status', detail: { upstream: 'none' } });
|
|
55
|
+
}
|
|
56
|
+
const tb = taobao(ctx);
|
|
57
|
+
const page = await api.goodsPage(tb, await resolveSellerItem(tb, ctx.args.user));
|
|
58
|
+
return norm.seller(api.parseSeller(tb, page), page.html);
|
|
59
|
+
}
|
|
60
|
+
// ================================================================ msg
|
|
61
|
+
/** 消息记录:在一条连接上往更早翻,输出从旧到新;游标见 _shared/impaas.ts 的 history。 */
|
|
62
|
+
export async function msgHistory(ctx) {
|
|
63
|
+
const tb = taobao(ctx);
|
|
64
|
+
tb.requireLogin();
|
|
65
|
+
const { cid } = resolveConversation(ctx.args.conversation, tb.myId);
|
|
66
|
+
return history(ctx, () => openIm(tb), cid, (m) => norm.historyMessage(m, cid));
|
|
67
|
+
}
|
|
68
|
+
/** 上传一张图,返回发图片消息要用的字段(上游 make_image 的参数)。 */
|
|
69
|
+
async function uploadImage(tb, input) {
|
|
70
|
+
const file = await readMedia(tb.http, input);
|
|
71
|
+
if (!file.contentType.startsWith('image/'))
|
|
72
|
+
throw new SpecificAIError('UNSUPPORTED', `淘宝私信只支持图片:${input}`);
|
|
73
|
+
const res = await api.uploadMedia(tb, file);
|
|
74
|
+
const media = norm.uploaded(res.object);
|
|
75
|
+
return {
|
|
76
|
+
media,
|
|
77
|
+
message: { type: 'image', file_id: res.object.fileId, image_url: res.object.url, size: res.object.size, width: media.width ?? 0, height: media.height ?? 0 },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 发私信。`--item`:打开商品页取卖家(get_goods_uid_encrypt_uid),建会话(create_chat),再发(send_msg);
|
|
82
|
+
* `--conversation`:直接发。文字和图片都有时依次发送,返回最后一条。
|
|
83
|
+
*/
|
|
84
|
+
export async function msgSend(ctx) {
|
|
85
|
+
const tb = taobao(ctx);
|
|
86
|
+
tb.requireLogin();
|
|
87
|
+
const o = ctx.options;
|
|
88
|
+
if (o.video != null)
|
|
89
|
+
throw new SpecificAIError('UNSUPPORTED', '淘宝私信不支持发视频');
|
|
90
|
+
if (o.to != null) {
|
|
91
|
+
throw new SpecificAIError('UNSUPPORTED', '淘宝私信不能直接按用户发送,需要先有会话', {
|
|
92
|
+
hint: '联系卖家用 --item <商品链接>;回复已有会话用 --conversation <会话 ID>',
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
let target;
|
|
96
|
+
if (o.item != null) {
|
|
97
|
+
const seller = api.parseSeller(tb, await api.goodsPage(tb, await resolveItem(tb, o.item)));
|
|
98
|
+
target = { cid: null, peer: seller.uid, encryptUid: seller.encrypt_uid };
|
|
99
|
+
}
|
|
100
|
+
else
|
|
101
|
+
target = resolveConversation(o.conversation, tb.myId);
|
|
102
|
+
if (target.peer === tb.myId)
|
|
103
|
+
throw new SpecificAIError('USAGE', '不能给自己发私信');
|
|
104
|
+
const outgoing = [];
|
|
105
|
+
if (ctx.args.text != null)
|
|
106
|
+
outgoing.push({ media: [], message: { type: 'text', text: ctx.args.text } });
|
|
107
|
+
for (const input of o.image ?? []) {
|
|
108
|
+
const img = await uploadImage(tb, input);
|
|
109
|
+
outgoing.push({ media: [img.media], message: img.message });
|
|
110
|
+
}
|
|
111
|
+
const im = await openIm(tb);
|
|
112
|
+
try {
|
|
113
|
+
await im.ready();
|
|
114
|
+
let cid = target.cid;
|
|
115
|
+
if (cid == null) {
|
|
116
|
+
cid = createdCid(await im.request(createChatFrame(tb.myId, target.encryptUid)));
|
|
117
|
+
if (!cid)
|
|
118
|
+
throw new SpecificAIError('UPSTREAM', '建立会话失败:响应里没有会话 ID');
|
|
119
|
+
}
|
|
120
|
+
let last;
|
|
121
|
+
for (const { media, message } of outgoing) {
|
|
122
|
+
// 照上游(taobao_live.py 的 f"cntaobao{self.nk}")拼 cookie `_nk_` 的原值,中文昵称也不解码
|
|
123
|
+
const frame = sendMsgFrame(tb.myId, imId(cid), target.peer, IM_DOMAIN + tb.rawNick, message);
|
|
124
|
+
const res = await im.request(frame);
|
|
125
|
+
const b = res.body ?? {};
|
|
126
|
+
last = n.message({
|
|
127
|
+
id: n.id(b.messageId ?? frame.body[0].uuid),
|
|
128
|
+
conversation_id: cid,
|
|
129
|
+
from: tb.me(),
|
|
130
|
+
type: message.type,
|
|
131
|
+
text: message.type === 'text' ? message.text : null,
|
|
132
|
+
media,
|
|
133
|
+
created_at: n.time(b.createAt ?? rand.now()),
|
|
134
|
+
}, res);
|
|
135
|
+
}
|
|
136
|
+
return last;
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
im.close();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** 推送帧里别人发来的聊天消息。 */
|
|
143
|
+
function pushedMessages(tb, frame) {
|
|
144
|
+
const out = [];
|
|
145
|
+
for (const decoded of pushedPayloads(frame, decrypt, tb.ctx)) {
|
|
146
|
+
const message = norm.pushMessage(decoded);
|
|
147
|
+
if (!message)
|
|
148
|
+
continue;
|
|
149
|
+
// 自己发出的消息(上游按 sender_nick 判断)不输出
|
|
150
|
+
const nick = decoded?.['1']?.['10']?.sender_nick;
|
|
151
|
+
if (message.from?.id === tb.myId || (nick != null && (nick === IM_DOMAIN + tb.rawNick || nick === IM_DOMAIN + tb.nick)))
|
|
152
|
+
continue;
|
|
153
|
+
out.push(message);
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
export function msgListen(ctx) {
|
|
158
|
+
return (async function* () {
|
|
159
|
+
const tb = taobao(ctx);
|
|
160
|
+
tb.requireLogin();
|
|
161
|
+
yield* listen(ctx, () => openIm(tb, { heartbeat: true }), (frame) => pushedMessages(tb, frame));
|
|
162
|
+
})();
|
|
163
|
+
}
|
|
164
|
+
// ================================================================ media
|
|
165
|
+
export async function mediaUpload(ctx) {
|
|
166
|
+
const tb = taobao(ctx);
|
|
167
|
+
tb.requireLogin();
|
|
168
|
+
const file = await readMedia(tb.http, ctx.args.file);
|
|
169
|
+
if (!file.contentType.startsWith('image/'))
|
|
170
|
+
throw new SpecificAIError('UNSUPPORTED', `淘宝只支持上传图片:${ctx.args.file}`);
|
|
171
|
+
return norm.uploaded((await api.uploadMedia(tb, file)).object);
|
|
172
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsonDumps } from '../../../core/py.js';
|
|
2
|
+
import { impaas } from '../../_shared/impaas.js';
|
|
3
|
+
import { accessToken } from './client.js';
|
|
4
|
+
import { IM_APP_KEY, IM_DOMAIN, IM_UA, WSS_URL, wsHeaders } from './profile.js';
|
|
5
|
+
import { generateMid, generateUuid } from './sign.js';
|
|
6
|
+
/**
|
|
7
|
+
* 私信:钉钉 IMPaaS 的 wss 长连(上游 taobao_live.py)。
|
|
8
|
+
* 连接、注册、ack、心跳、翻页与闲鱼共用 _shared/impaas.ts;这里是淘宝的参数和建会话、发消息的帧。
|
|
9
|
+
*/
|
|
10
|
+
export { mockConnect } from '../../_shared/impaas.js';
|
|
11
|
+
const im = impaas({
|
|
12
|
+
url: WSS_URL,
|
|
13
|
+
headers: wsHeaders,
|
|
14
|
+
appKey: IM_APP_KEY,
|
|
15
|
+
ua: IM_UA,
|
|
16
|
+
domain: IM_DOMAIN,
|
|
17
|
+
mid: generateMid,
|
|
18
|
+
token: accessToken,
|
|
19
|
+
});
|
|
20
|
+
/** `<id>@cntaobao`;plainId 去掉 `@cntaobao` 后缀。 */
|
|
21
|
+
export const { imId, plainId, regFrame, ackDiffFrame, heartbeatFrame, listFrame, open: openIm } = im;
|
|
22
|
+
/** create_chat:用卖家的 encrypt_uid 建单聊会话。 */
|
|
23
|
+
export function createChatFrame(myId, encryptUid) {
|
|
24
|
+
return {
|
|
25
|
+
lwp: '/r/SingleChatConversation/create',
|
|
26
|
+
headers: { mid: generateMid() },
|
|
27
|
+
body: [{ pairFirst: imId(myId), bizType: '11001', ctx: { createConversationCtx: '{"encryptUid":"' + encryptUid + '"}', selfBizDomain: 'taobao' } }],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** send_msg:cid 带 `@cntaobao` 后缀;图片是 base64 的 JSON,放在 custom 里。 */
|
|
31
|
+
export function sendMsgFrame(myId, cid, toId, senderNick, message) {
|
|
32
|
+
const extension = { senderBizDomain: 'taobao', receiverBizDomain: 'taobao', sender_nick: senderNick };
|
|
33
|
+
const content = { contentType: null };
|
|
34
|
+
if (message.type === 'text') {
|
|
35
|
+
content.contentType = 1;
|
|
36
|
+
content.text = { extension: { sender_nick: senderNick }, content: message.text };
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
delete extension.sender_nick;
|
|
40
|
+
const data = { fileId: message.file_id, size: message.size, url: message.image_url, width: message.width, height: message.height, isOriginal: 1, suffix: 'png' };
|
|
41
|
+
content.contentType = 101;
|
|
42
|
+
content.custom = { type: 7, data: Buffer.from(jsonDumps(data)).toString('base64') };
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
lwp: '/r/MessageSend/sendByReceiverScope',
|
|
46
|
+
headers: { mid: generateMid() },
|
|
47
|
+
body: [
|
|
48
|
+
{
|
|
49
|
+
cid,
|
|
50
|
+
uuid: generateUuid(),
|
|
51
|
+
conversationType: 1,
|
|
52
|
+
redPointPolicy: 0,
|
|
53
|
+
extension,
|
|
54
|
+
content,
|
|
55
|
+
ctx: { senderBizDomain: 'taobao', receiverBizDomain: 'taobao' },
|
|
56
|
+
},
|
|
57
|
+
{ actualReceivers: [imId(myId), imId(toId)] },
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** 从 SingleChatConversation/create 的响应里取会话 ID;响应里没有 cid 字段时,在整个 body 里找形如会话 ID 的字符串。 */
|
|
62
|
+
export function createdCid(res) {
|
|
63
|
+
const direct = im.createdCid(res);
|
|
64
|
+
if (direct)
|
|
65
|
+
return direct;
|
|
66
|
+
const m = /"(\d+\.\d+-\d+\.\d+#\d+)(?:@cntaobao)?"/.exec(JSON.stringify(res.body ?? null));
|
|
67
|
+
return m ? m[1] : null;
|
|
68
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as n from '../../../core/normalize.js';
|
|
2
|
+
import { jsonLoads } from '../../../core/py.js';
|
|
3
|
+
import { IM_DOMAIN } from './profile.js';
|
|
4
|
+
import { plainId } from './im.js';
|
|
5
|
+
/** 淘宝原始对象 → 归一化类型(AGENTS 6.2)。 */
|
|
6
|
+
/** 私信里的 sender_nick 带 `cntaobao` 前缀。 */
|
|
7
|
+
function nickOf(senderNick) {
|
|
8
|
+
const s = n.str(senderNick);
|
|
9
|
+
return s == null ? null : s.startsWith(IM_DOMAIN) ? s.slice(IM_DOMAIN.length) || null : s;
|
|
10
|
+
}
|
|
11
|
+
function ref(uid, senderNick) {
|
|
12
|
+
return n.userRef({ id: plainId(uid) || null, name: nickOf(senderNick), url: null });
|
|
13
|
+
}
|
|
14
|
+
/** 自定义消息的 data:上游发送时是 base64 的 JSON;推送里可能已经是 JSON 字符串。 */
|
|
15
|
+
function customData(data) {
|
|
16
|
+
if (data == null || typeof data === 'object')
|
|
17
|
+
return data ?? null;
|
|
18
|
+
const s = String(data);
|
|
19
|
+
for (const text of [s, Buffer.from(s, 'base64').toString('utf8')]) {
|
|
20
|
+
try {
|
|
21
|
+
return jsonLoads(text);
|
|
22
|
+
}
|
|
23
|
+
catch { }
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
/** contentType 1 为文字,101 为自定义消息(type 7 是图片)。 */
|
|
28
|
+
function body(contentType, text, custom) {
|
|
29
|
+
const type = Number(contentType);
|
|
30
|
+
if (type === 1)
|
|
31
|
+
return { type: 'text', text: n.str(text), media: [] };
|
|
32
|
+
if (type === 101 && custom) {
|
|
33
|
+
const data = customData(custom.data);
|
|
34
|
+
const url = n.url(data?.url);
|
|
35
|
+
if (url && (Number(custom.type) === 7 || data?.fileId != null)) {
|
|
36
|
+
return {
|
|
37
|
+
type: 'image',
|
|
38
|
+
text: null,
|
|
39
|
+
media: [n.media({ id: n.idOrNull(data.fileId), type: 'image', url, width: n.count(data.width), height: n.count(data.height) })],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return { type: 'card', text: n.str(custom.summary) ?? (typeof data?.title === 'string' ? data.title : null), media: [] };
|
|
43
|
+
}
|
|
44
|
+
return { type: 'other', text: n.str(text), media: [] };
|
|
45
|
+
}
|
|
46
|
+
/** listUserMessages 返回的一条(userMessageModels[])。 */
|
|
47
|
+
export function historyMessage(model, cid) {
|
|
48
|
+
const m = model?.message ?? {};
|
|
49
|
+
const content = m.content ?? {};
|
|
50
|
+
const text = typeof content.text === 'string' ? content.text : (content.text?.content ?? content.text?.text);
|
|
51
|
+
return n.message({
|
|
52
|
+
id: n.id(m.messageId ?? m.uuid),
|
|
53
|
+
conversation_id: plainId(m.cid) || cid,
|
|
54
|
+
from: ref(m.sender?.uid, m.extension?.sender_nick),
|
|
55
|
+
...body(content.contentType, text, content.custom ?? null),
|
|
56
|
+
created_at: n.time(m.createAt),
|
|
57
|
+
}, model);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 推送里解码后的消息(上游 handle_message):字段是编号,
|
|
61
|
+
* 1.1.1 发送者、1.2 会话、1.3 消息 ID、1.5 时间、1.6 内容(1 类型、2.1 文字、3 自定义)、1.10.sender_nick 昵称。
|
|
62
|
+
* 不是聊天消息的推送(会话状态、已读等)返回 null。
|
|
63
|
+
*/
|
|
64
|
+
export function pushMessage(decoded) {
|
|
65
|
+
const m = decoded?.['1'];
|
|
66
|
+
if (!m || typeof m !== 'object' || Array.isArray(m))
|
|
67
|
+
return null;
|
|
68
|
+
const sender = m['1']?.['1'];
|
|
69
|
+
const cid = m['2'];
|
|
70
|
+
const c = m['6'];
|
|
71
|
+
if (typeof sender !== 'string' || typeof cid !== 'string' || !c || typeof c !== 'object')
|
|
72
|
+
return null;
|
|
73
|
+
const custom = c['3'] && typeof c['3'] === 'object' ? { type: c['3']['4'], data: c['3']['5'], summary: c['3']['2'] } : null;
|
|
74
|
+
return n.message({
|
|
75
|
+
id: n.id(m['3']),
|
|
76
|
+
conversation_id: plainId(cid),
|
|
77
|
+
from: ref(sender, m['10']?.sender_nick),
|
|
78
|
+
...body(c['1'], c['2']?.['1'], custom),
|
|
79
|
+
created_at: n.time(m['5']),
|
|
80
|
+
}, decoded);
|
|
81
|
+
}
|
|
82
|
+
/** upload_media 返回的 object:pix 为 `宽x高`。 */
|
|
83
|
+
export function uploaded(obj) {
|
|
84
|
+
const [w, h] = String(obj?.pix ?? '').split('x');
|
|
85
|
+
return n.media({ id: n.idOrNull(obj?.fileId), type: 'image', url: n.url(obj?.url) ?? '', width: n.count(w), height: n.count(h) }, obj);
|
|
86
|
+
}
|
|
87
|
+
function jsonString(raw) {
|
|
88
|
+
if (raw == null)
|
|
89
|
+
return null;
|
|
90
|
+
try {
|
|
91
|
+
return n.str(JSON.parse(`"${raw}"`));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return n.str(raw);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** 商品页里的卖家(上游 get_goods_uid_encrypt_uid)。昵称、店铺名、店铺链接能取到就带上。 */
|
|
98
|
+
export function seller(s, html) {
|
|
99
|
+
const nick = jsonString(/"sellerNick":"(.*?)"/.exec(html)?.[1]);
|
|
100
|
+
const shop = jsonString(/"shopName":"(.*?)"/.exec(html)?.[1]);
|
|
101
|
+
const shopUrl = n.url(jsonString(/"shopUrl":"(.*?)"/.exec(html)?.[1]));
|
|
102
|
+
return n.user({ id: s.uid, name: shop ?? nick, handle: nick, url: shopUrl }, { ...s, seller_nick: nick, shop_name: shop, shop_url: shopUrl });
|
|
103
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 淘宝 web 端的常量与请求头(上游 taobao_apis.py、taobao_live.py)。
|
|
3
|
+
* 请求头的名字、大小写和顺序照抄上游,对拍测试逐字节比较。
|
|
4
|
+
*/
|
|
5
|
+
export const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36';
|
|
6
|
+
/** 上游私信长连握手用的是 Chrome 133 的 UA(taobao_live.py),照抄。 */
|
|
7
|
+
const WS_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36';
|
|
8
|
+
const SEC_CH_UA = '"Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146"';
|
|
9
|
+
export const BROWSER = 'chrome_146';
|
|
10
|
+
export const COOKIE_DOMAIN = '.taobao.com';
|
|
11
|
+
/** mtop 的 appKey,也参与签名。 */
|
|
12
|
+
export const APP_KEY = '12574478';
|
|
13
|
+
/** 钉钉 IMPaaS 的 app-key。 */
|
|
14
|
+
export const IM_APP_KEY = '3ce2dacdc7c0c43ad7bc7f9bc7d7a1b8';
|
|
15
|
+
/** 私信里用户 ID、会话 ID 的后缀。 */
|
|
16
|
+
export const IM_DOMAIN = 'cntaobao';
|
|
17
|
+
export const LOGIN_TOKEN_URL = 'https://h5api.m.taobao.com/h5/mtop.taobao.login.token.get.h5/2.0/';
|
|
18
|
+
export const UPLOAD_URL = 'https://stream-upload.taobao.com/api/upload.api';
|
|
19
|
+
export const WSS_URL = 'wss://wss-cntaobao.dingtalk.com/';
|
|
20
|
+
/** /reg 帧里的 ua。 */
|
|
21
|
+
export const IM_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 DingTalk(2.1.5) OS(Windows/10) Browser(Chrome/146.0.0.0) DingWeb/2.1.5 IMPaaS DingWeb/2.1.5';
|
|
22
|
+
/** get_token 的请求头。 */
|
|
23
|
+
export const TOKEN_HEADERS = [
|
|
24
|
+
['accept', '*/*'],
|
|
25
|
+
['accept-language', 'en,zh-CN;q=0.9,zh;q=0.8,zh-TW;q=0.7,ja;q=0.6'],
|
|
26
|
+
['cache-control', 'no-cache'],
|
|
27
|
+
['pragma', 'no-cache'],
|
|
28
|
+
['referer', 'https://market.m.taobao.com/'],
|
|
29
|
+
['sec-ch-ua', SEC_CH_UA],
|
|
30
|
+
['sec-ch-ua-mobile', '?0'],
|
|
31
|
+
['sec-ch-ua-platform', '"Windows"'],
|
|
32
|
+
['sec-fetch-dest', 'script'],
|
|
33
|
+
['sec-fetch-mode', 'no-cors'],
|
|
34
|
+
['sec-fetch-site', 'same-site'],
|
|
35
|
+
['user-agent', UA],
|
|
36
|
+
];
|
|
37
|
+
/** get_goods_uid_encrypt_uid 打开商品页的请求头。 */
|
|
38
|
+
export const DOC_HEADERS = [
|
|
39
|
+
['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'],
|
|
40
|
+
['accept-language', 'en'],
|
|
41
|
+
['cache-control', 'no-cache'],
|
|
42
|
+
['pragma', 'no-cache'],
|
|
43
|
+
['priority', 'u=0, i'],
|
|
44
|
+
['referer', 'https://www.taobao.com/'],
|
|
45
|
+
['sec-ch-ua', SEC_CH_UA],
|
|
46
|
+
['sec-ch-ua-mobile', '?0'],
|
|
47
|
+
['sec-ch-ua-platform', '"Windows"'],
|
|
48
|
+
['sec-fetch-dest', 'document'],
|
|
49
|
+
['sec-fetch-mode', 'navigate'],
|
|
50
|
+
['sec-fetch-site', 'same-origin'],
|
|
51
|
+
['sec-fetch-user', '?1'],
|
|
52
|
+
['upgrade-insecure-requests', '1'],
|
|
53
|
+
['user-agent', UA],
|
|
54
|
+
];
|
|
55
|
+
/** upload_media 的请求头。 */
|
|
56
|
+
export const UPLOAD_HEADERS = [
|
|
57
|
+
['Accept', '*/*'],
|
|
58
|
+
['Accept-Language', 'en,zh-CN;q=0.9,zh;q=0.8,zh-TW;q=0.7,ja;q=0.6'],
|
|
59
|
+
['Cache-Control', 'no-cache'],
|
|
60
|
+
['Connection', 'keep-alive'],
|
|
61
|
+
['Origin', 'https://market.m.taobao.com'],
|
|
62
|
+
['Pragma', 'no-cache'],
|
|
63
|
+
['Referer', 'https://market.m.taobao.com/'],
|
|
64
|
+
['Sec-Fetch-Dest', 'empty'],
|
|
65
|
+
['Sec-Fetch-Mode', 'cors'],
|
|
66
|
+
['Sec-Fetch-Site', 'same-site'],
|
|
67
|
+
['User-Agent', UA],
|
|
68
|
+
['sec-ch-ua', SEC_CH_UA],
|
|
69
|
+
['sec-ch-ua-mobile', '?0'],
|
|
70
|
+
['sec-ch-ua-platform', '"Windows"'],
|
|
71
|
+
];
|
|
72
|
+
/** 私信长连的握手头(Cookie 由调用方填)。Host / Connection 由 WebSocket 库自己管,真正连接时去掉。 */
|
|
73
|
+
export function wsHeaders(cookie) {
|
|
74
|
+
return [
|
|
75
|
+
['Cookie', cookie],
|
|
76
|
+
['Host', 'wss-cntaobao.dingtalk.com'],
|
|
77
|
+
['Connection', 'Upgrade'],
|
|
78
|
+
['Pragma', 'no-cache'],
|
|
79
|
+
['Cache-Control', 'no-cache'],
|
|
80
|
+
['User-Agent', WS_UA],
|
|
81
|
+
['Origin', 'https://www.cntaobao.com'],
|
|
82
|
+
['Accept-Encoding', 'gzip, deflate, br, zstd'],
|
|
83
|
+
['Accept-Language', 'zh-CN,zh;q=0.9'],
|
|
84
|
+
];
|
|
85
|
+
}
|