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,305 @@
|
|
|
1
|
+
import { GUEST } from '../../../core/auth-store.js';
|
|
2
|
+
import { SpecificAIError } from '../../../core/errors.js';
|
|
3
|
+
import { readMedia } from '../../../core/files.js';
|
|
4
|
+
import { cookieCredential, finishLogin, freshCredential, loginContext, poll, showQrcode } from '../../../core/login.js';
|
|
5
|
+
import * as n from '../../../core/normalize.js';
|
|
6
|
+
import * as rand from '../../../core/rand.js';
|
|
7
|
+
import { history, listen, pushedPayloads } from '../../_shared/impaas.js';
|
|
8
|
+
import * as api from './api.js';
|
|
9
|
+
import { unsignedMtop, Xianyu, xianyu } from './client.js';
|
|
10
|
+
import { createChatFrame, createdCid, FIRST_CURSOR, openIm, sendMsgFrame } from './im.js';
|
|
11
|
+
import * as norm from './normalize.js';
|
|
12
|
+
import { COOKIE_DOMAIN, itemUrl } from './profile.js';
|
|
13
|
+
import { resolveConversation, resolveItem, resolveUser } from './resolve.js';
|
|
14
|
+
import { decrypt } from './sign.js';
|
|
15
|
+
const isAuthError = (err) => err instanceof SpecificAIError && (err.code === 'AUTH_REQUIRED' || err.code === 'AUTH_EXPIRED');
|
|
16
|
+
/** 常驻进程续期 cookie 的间隔(上游 user_alive 为 600 秒)。 */
|
|
17
|
+
const KEEPALIVE_MS = 600_000;
|
|
18
|
+
// ================================================================ auth
|
|
19
|
+
async function completeLogin(ctx, x) {
|
|
20
|
+
if (!x.myId)
|
|
21
|
+
throw new SpecificAIError('AUTH_REQUIRED', '登录没有成功:cookie 里没有 unb', { hint: `specific-ai xianyu auth login` });
|
|
22
|
+
return finishLogin(ctx, x.ctx.credential, x.me());
|
|
23
|
+
}
|
|
24
|
+
/** 扫码登录(上游 qrcode_login)。 */
|
|
25
|
+
async function qrcodeLogin(ctx) {
|
|
26
|
+
const x = new Xianyu({ ...ctx, account: GUEST, credential: freshCredential(ctx, 'qrcode') });
|
|
27
|
+
await x.buildInitialCookies();
|
|
28
|
+
const cna = x.jar.get('cna', COOKIE_DOMAIN) ?? x.jar.get('cna', '.mmstat.com') ?? '';
|
|
29
|
+
const cookie2 = x.jar.get('cookie2', COOKIE_DOMAIN) ?? '';
|
|
30
|
+
await api.miniLogin(x);
|
|
31
|
+
const csrf = x.jar.get('XSRF-TOKEN', 'passport.goofish.com') ?? '';
|
|
32
|
+
const qr = await api.qrGenerate(x, csrf, cookie2);
|
|
33
|
+
await showQrcode(ctx, qr.codeContent, '请用闲鱼 App 扫码(左上角 → 扫一扫)并确认');
|
|
34
|
+
let last = '';
|
|
35
|
+
const token = await poll(async () => {
|
|
36
|
+
const d = await api.qrQuery(x, csrf, cookie2, cna, qr);
|
|
37
|
+
const status = String(d.qrCodeStatus ?? '');
|
|
38
|
+
if (status !== last)
|
|
39
|
+
ctx.log.debug(`扫码状态:${status || '(空)'}`, { keys: Object.keys(d), titleMsg: d.titleMsg ?? null });
|
|
40
|
+
if (status !== last && status === 'SCANNED')
|
|
41
|
+
ctx.log.info('已扫码,请在手机上确认');
|
|
42
|
+
last = status;
|
|
43
|
+
if (status === 'CONFIRMED')
|
|
44
|
+
return String(d.token ?? d.lgToken ?? '');
|
|
45
|
+
if (status === 'EXPIRED')
|
|
46
|
+
throw new SpecificAIError('AUTH_REQUIRED', '二维码已过期', { hint: '重新执行 specific-ai xianyu auth login' });
|
|
47
|
+
return undefined;
|
|
48
|
+
}, { interval: 3000, timeout: 120_000, what: '扫码' });
|
|
49
|
+
// CONFIRMED 响应的 Set-Cookie 里一般已经有 unb 等登录态;有 token 时再走一次 login.do
|
|
50
|
+
if (token)
|
|
51
|
+
await api.loginByToken(x, token, cna);
|
|
52
|
+
else if (!x.myId)
|
|
53
|
+
throw new SpecificAIError('AUTH_REQUIRED', '扫码超时,未完成登录', { hint: '重新执行 specific-ai xianyu auth login' });
|
|
54
|
+
// 登录后 _m_h5_tk 会变,空签名调一次 mtop 刷新
|
|
55
|
+
await x.http.request(unsignedMtop('mtop.idle.web.user.page.nav'));
|
|
56
|
+
return completeLogin(ctx, x);
|
|
57
|
+
}
|
|
58
|
+
export async function authLogin(ctx) {
|
|
59
|
+
if (ctx.options.method === 'cookie') {
|
|
60
|
+
const credential = cookieCredential(ctx, COOKIE_DOMAIN);
|
|
61
|
+
const x = new Xianyu(loginContext(ctx, credential));
|
|
62
|
+
const hint = '复制浏览器里登录后的闲鱼 cookie(需要包含 unb、_m_h5_tk、cookie2)';
|
|
63
|
+
if (!x.myId)
|
|
64
|
+
throw new SpecificAIError('USAGE', 'cookie 里没有 unb,不是登录后的闲鱼 cookie', { hint });
|
|
65
|
+
try {
|
|
66
|
+
await api.refreshToken(x);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
if (isAuthError(err))
|
|
70
|
+
throw new SpecificAIError('AUTH_REQUIRED', '登录没有成功:cookie 无效或已过期', { hint });
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
return completeLogin(ctx, x);
|
|
74
|
+
}
|
|
75
|
+
return qrcodeLogin(ctx);
|
|
76
|
+
}
|
|
77
|
+
export async function authStatus(ctx) {
|
|
78
|
+
const off = { logged_in: false, user: null, method: null, expires_at: null };
|
|
79
|
+
const x = new Xianyu(ctx);
|
|
80
|
+
// 没登录时是一份空凭证,没有 unb
|
|
81
|
+
if (!x.myId)
|
|
82
|
+
return off;
|
|
83
|
+
try {
|
|
84
|
+
await api.refreshToken(x);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
if (isAuthError(err))
|
|
88
|
+
return off;
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
const expires = ['cookie2', 'sgcookie', 'unb'].map((name) => x.jar.cookies.find((c) => c.name === name)?.expires).find((e) => e != null);
|
|
92
|
+
return { logged_in: true, user: x.me(), method: ctx.credential.method, expires_at: n.time(expires) };
|
|
93
|
+
}
|
|
94
|
+
// ================================================================ user
|
|
95
|
+
export async function userGet(ctx) {
|
|
96
|
+
if (ctx.args.user !== 'me') {
|
|
97
|
+
throw new SpecificAIError('NOT_IMPLEMENTED', 'xianyu 的 user get 只支持 me,查询他人尚未实现', { detail: { upstream: 'none' } });
|
|
98
|
+
}
|
|
99
|
+
const x = xianyu(ctx);
|
|
100
|
+
x.requireLogin();
|
|
101
|
+
return norm.me((await api.refreshToken(x)).data, x.me());
|
|
102
|
+
}
|
|
103
|
+
// ================================================================ item
|
|
104
|
+
export async function itemGet(ctx) {
|
|
105
|
+
const x = xianyu(ctx);
|
|
106
|
+
const id = await resolveItem(x, ctx.args.item);
|
|
107
|
+
return norm.item((await api.itemInfo(x, id)).data, x.myId);
|
|
108
|
+
}
|
|
109
|
+
export async function itemPublish(ctx) {
|
|
110
|
+
const x = xianyu(ctx);
|
|
111
|
+
x.requireLogin();
|
|
112
|
+
const o = ctx.options;
|
|
113
|
+
const desc = (o.text ?? o.title);
|
|
114
|
+
if (!desc)
|
|
115
|
+
throw new SpecificAIError('USAGE', '闲鱼发布需要 --text(商品描述,同时用作标题)', { hint: 'specific-ai xianyu item publish --text <描述> --image <图片> --price <元>' });
|
|
116
|
+
if (o.shipping === 'fixed' && o.postage == null)
|
|
117
|
+
throw new SpecificAIError('USAGE', '--shipping fixed 需要 --postage(运费,元)');
|
|
118
|
+
// 上游不填价格时(price=None)不看原价
|
|
119
|
+
if (o.originalPrice != null && o.price == null)
|
|
120
|
+
throw new SpecificAIError('USAGE', '--original-price 要和 --price 一起用');
|
|
121
|
+
const images = [];
|
|
122
|
+
for (const input of o.image ?? []) {
|
|
123
|
+
const file = await readMedia(x.http, input);
|
|
124
|
+
if (!file.contentType.startsWith('image/'))
|
|
125
|
+
throw new SpecificAIError('UNSUPPORTED', `闲鱼只支持上传图片:${input}`);
|
|
126
|
+
images.push(file);
|
|
127
|
+
}
|
|
128
|
+
const price = o.price != null ? { current: Number(o.price), original: Number(o.originalPrice ?? 0) } : null;
|
|
129
|
+
const res = await api.publish(x, {
|
|
130
|
+
images,
|
|
131
|
+
desc,
|
|
132
|
+
price,
|
|
133
|
+
shipping: o.shipping ?? 'free',
|
|
134
|
+
postage: Number(o.postage ?? 0),
|
|
135
|
+
pickup: Boolean(o.pickup),
|
|
136
|
+
});
|
|
137
|
+
const id = n.id(res.data?.itemId ?? res.data?.item?.itemId);
|
|
138
|
+
return n.item({
|
|
139
|
+
id,
|
|
140
|
+
kind: 'goods',
|
|
141
|
+
url: id ? itemUrl(id) : null,
|
|
142
|
+
title: desc,
|
|
143
|
+
text: desc,
|
|
144
|
+
author: x.me(),
|
|
145
|
+
price: price ? n.price(price.current) : null,
|
|
146
|
+
status: 'on_sale',
|
|
147
|
+
}, res);
|
|
148
|
+
}
|
|
149
|
+
// ================================================================ msg
|
|
150
|
+
/** 消息记录:在一条连接上往更早翻,输出从旧到新;游标见 _shared/impaas.ts 的 history。 */
|
|
151
|
+
export async function msgHistory(ctx) {
|
|
152
|
+
const x = xianyu(ctx);
|
|
153
|
+
x.requireLogin();
|
|
154
|
+
const cid = resolveConversation(ctx.args.conversation);
|
|
155
|
+
return history(ctx, () => openIm(x), cid, (m) => norm.historyMessage(m, cid));
|
|
156
|
+
}
|
|
157
|
+
/** 上传一张图,返回发图片消息要用的字段(上游 make_image 的参数)。 */
|
|
158
|
+
async function uploadImage(x, input) {
|
|
159
|
+
const file = await readMedia(x.http, input);
|
|
160
|
+
if (!file.contentType.startsWith('image/'))
|
|
161
|
+
throw new SpecificAIError('UNSUPPORTED', `闲鱼私信只支持图片:${input}`);
|
|
162
|
+
const media = norm.uploaded((await api.uploadMedia(x, file)).object);
|
|
163
|
+
if (!media.url)
|
|
164
|
+
throw new SpecificAIError('UPSTREAM', '图片上传没有返回地址');
|
|
165
|
+
return { media, message: { type: 'image', image_url: media.url, width: media.width ?? 0, height: media.height ?? 0 } };
|
|
166
|
+
}
|
|
167
|
+
/** 找对方时最多往前翻几页(每页 20 条)。 */
|
|
168
|
+
const PEER_PAGES = 10;
|
|
169
|
+
/**
|
|
170
|
+
* 已有会话的对方:照上游 list_all_conversations 在同一条连接上往更早翻,找不是自己发的消息的发送者;
|
|
171
|
+
* 找不到(对方从没回过)就报错,让用户改用 --to。
|
|
172
|
+
*/
|
|
173
|
+
async function peerOf(im, x, cid) {
|
|
174
|
+
let pages = 0;
|
|
175
|
+
let more = false;
|
|
176
|
+
for await (const page of im.historyPages(cid, FIRST_CURSOR)) {
|
|
177
|
+
for (const model of page.models) {
|
|
178
|
+
const sender = norm.historyMessage(model, cid).from?.id;
|
|
179
|
+
if (sender && sender !== x.myId)
|
|
180
|
+
return sender;
|
|
181
|
+
}
|
|
182
|
+
more = page.hasMore;
|
|
183
|
+
if (++pages >= PEER_PAGES)
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
const where = more ? `最近 ${pages} 页消息` : '全部消息记录';
|
|
187
|
+
throw new SpecificAIError('USAGE', `会话 ${cid} 的${where}里只有你自己发的消息,确定不了收信人`, {
|
|
188
|
+
hint: '用 --to <对方用户>(可加 --item <商品>)发送',
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/** 要新建(或取回已有)会话的对方与商品;null 表示发到 --conversation 指定的会话。 */
|
|
192
|
+
async function chatTarget(x, o) {
|
|
193
|
+
if (o.to != null) {
|
|
194
|
+
// 主动发给指定用户(上游 create_chat 不给 item_id 时用写死的默认商品);带 --item 时就这件商品联系,卖家可以借此联系买家
|
|
195
|
+
const peer = await resolveUser(x, o.to);
|
|
196
|
+
if (peer === x.myId)
|
|
197
|
+
throw new SpecificAIError('USAGE', '不能给自己发私信');
|
|
198
|
+
return { peer, ...(o.item != null ? { item: await resolveItem(x, o.item) } : {}) };
|
|
199
|
+
}
|
|
200
|
+
if (o.item == null)
|
|
201
|
+
return null;
|
|
202
|
+
// 只给商品:取商品详情拿卖家
|
|
203
|
+
const id = await resolveItem(x, o.item);
|
|
204
|
+
const seller = norm.item((await api.itemInfo(x, id)).data).author?.id;
|
|
205
|
+
if (!seller)
|
|
206
|
+
throw new SpecificAIError('UPSTREAM', '商品详情里没有卖家 ID');
|
|
207
|
+
if (seller === x.myId)
|
|
208
|
+
throw new SpecificAIError('USAGE', '这是你自己的商品,不能给自己发私信', { hint: '联系买家用 --to <买家> --item <商品>' });
|
|
209
|
+
return { peer: seller, item: id };
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* 发私信:
|
|
213
|
+
* - `--to <user>`:建会话(create_chat,默认商品)再发(send_msg);加 `--item` 时按这件商品建会话;
|
|
214
|
+
* - `--item`:取商品详情拿卖家,建会话,再发;
|
|
215
|
+
* - `--conversation`:发到已有会话,对方从消息记录里找。
|
|
216
|
+
* 文字和图片都有时依次发送,返回最后一条。
|
|
217
|
+
*/
|
|
218
|
+
export async function msgSend(ctx) {
|
|
219
|
+
const x = xianyu(ctx);
|
|
220
|
+
x.requireLogin();
|
|
221
|
+
const o = ctx.options;
|
|
222
|
+
if (o.video != null)
|
|
223
|
+
throw new SpecificAIError('UNSUPPORTED', '闲鱼私信不支持发视频');
|
|
224
|
+
const chat = await chatTarget(x, o);
|
|
225
|
+
const outgoing = [];
|
|
226
|
+
if (ctx.args.text != null)
|
|
227
|
+
outgoing.push({ media: [], message: { type: 'text', text: ctx.args.text } });
|
|
228
|
+
for (const input of o.image ?? []) {
|
|
229
|
+
const img = await uploadImage(x, input);
|
|
230
|
+
outgoing.push({ media: [img.media], message: img.message });
|
|
231
|
+
}
|
|
232
|
+
const im = await openIm(x);
|
|
233
|
+
try {
|
|
234
|
+
await im.ready();
|
|
235
|
+
let cid;
|
|
236
|
+
let peer;
|
|
237
|
+
if (chat) {
|
|
238
|
+
const created = createdCid(await im.request(createChatFrame(x.myId, chat.peer, chat.item)));
|
|
239
|
+
if (!created)
|
|
240
|
+
throw new SpecificAIError('UPSTREAM', '建立会话失败:响应里没有会话 ID');
|
|
241
|
+
cid = created;
|
|
242
|
+
peer = chat.peer;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
cid = resolveConversation(o.conversation);
|
|
246
|
+
peer = await peerOf(im, x, cid);
|
|
247
|
+
}
|
|
248
|
+
let last;
|
|
249
|
+
for (const { media, message } of outgoing) {
|
|
250
|
+
const frame = sendMsgFrame(x.myId, cid, peer, message);
|
|
251
|
+
const res = await im.request(frame);
|
|
252
|
+
const b = res.body ?? {};
|
|
253
|
+
last = n.message({
|
|
254
|
+
id: n.id(b.messageId ?? frame.body[0].uuid),
|
|
255
|
+
conversation_id: cid,
|
|
256
|
+
from: x.me(),
|
|
257
|
+
type: message.type,
|
|
258
|
+
text: message.type === 'text' ? message.text : null,
|
|
259
|
+
media,
|
|
260
|
+
created_at: n.time(b.createAt ?? rand.now()),
|
|
261
|
+
}, res);
|
|
262
|
+
}
|
|
263
|
+
return last;
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
im.close();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/** 推送帧里别人发来的聊天消息。 */
|
|
270
|
+
function pushedMessages(x, frame) {
|
|
271
|
+
const out = [];
|
|
272
|
+
for (const decoded of pushedPayloads(frame, decrypt, x.ctx)) {
|
|
273
|
+
const message = norm.pushMessage(decoded);
|
|
274
|
+
// 自己发出的消息不输出
|
|
275
|
+
if (message && message.from?.id !== x.myId)
|
|
276
|
+
out.push(message);
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
export function msgListen(ctx) {
|
|
281
|
+
return (async function* () {
|
|
282
|
+
const x = xianyu(ctx);
|
|
283
|
+
x.requireLogin();
|
|
284
|
+
// 上游 user_alive:常驻时每 10 分钟调一次 refresh_token 续期 cookie
|
|
285
|
+
const keepalive = setInterval(() => {
|
|
286
|
+
api.refreshToken(x).catch((err) => ctx.log.warn(`续期失败:${err.message}`));
|
|
287
|
+
}, KEEPALIVE_MS);
|
|
288
|
+
keepalive.unref();
|
|
289
|
+
try {
|
|
290
|
+
yield* listen(ctx, () => openIm(x, { heartbeat: true }), (frame) => pushedMessages(x, frame));
|
|
291
|
+
}
|
|
292
|
+
finally {
|
|
293
|
+
clearInterval(keepalive);
|
|
294
|
+
}
|
|
295
|
+
})();
|
|
296
|
+
}
|
|
297
|
+
// ================================================================ media
|
|
298
|
+
export async function mediaUpload(ctx) {
|
|
299
|
+
const x = xianyu(ctx);
|
|
300
|
+
x.requireLogin();
|
|
301
|
+
const file = await readMedia(x.http, ctx.args.file);
|
|
302
|
+
if (!file.contentType.startsWith('image/'))
|
|
303
|
+
throw new SpecificAIError('UNSUPPORTED', `闲鱼只支持上传图片:${ctx.args.file}`);
|
|
304
|
+
return norm.uploaded((await api.uploadMedia(x, file)).object);
|
|
305
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { SpecificAIError } from '../../../core/errors.js';
|
|
2
|
+
import { jsonDumps } from '../../../core/py.js';
|
|
3
|
+
import { impaas } from '../../_shared/impaas.js';
|
|
4
|
+
import { getToken } from './api.js';
|
|
5
|
+
import { IM_APP_KEY, IM_DOMAIN, IM_UA, WSS_URL, wsHeaders } from './profile.js';
|
|
6
|
+
import { generateMid, generateUuid } from './sign.js';
|
|
7
|
+
/**
|
|
8
|
+
* 私信:钉钉 IMPaaS 的 wss 长连(上游 goofish_live.py 的 XianyuLive)。
|
|
9
|
+
* 连接、注册、ack、心跳、翻页与淘宝共用 _shared/impaas.ts;这里是闲鱼的参数和建会话、发消息的帧。
|
|
10
|
+
*/
|
|
11
|
+
export { FIRST_CURSOR, mockConnect } from '../../_shared/impaas.js';
|
|
12
|
+
/** 私信的 accessToken(上游 init 里取不到就退出)。 */
|
|
13
|
+
async function accessToken(x) {
|
|
14
|
+
const res = await getToken(x);
|
|
15
|
+
const token = res.data?.accessToken;
|
|
16
|
+
if (!token)
|
|
17
|
+
throw new SpecificAIError('UPSTREAM', '获取私信 token 失败', { detail: { ret: res.ret ?? null } });
|
|
18
|
+
return token;
|
|
19
|
+
}
|
|
20
|
+
const im = impaas({
|
|
21
|
+
url: WSS_URL,
|
|
22
|
+
headers: wsHeaders,
|
|
23
|
+
appKey: IM_APP_KEY,
|
|
24
|
+
ua: IM_UA,
|
|
25
|
+
domain: IM_DOMAIN,
|
|
26
|
+
mid: generateMid,
|
|
27
|
+
token: accessToken,
|
|
28
|
+
});
|
|
29
|
+
/** plainId 去掉 `@goofish` 后缀。 */
|
|
30
|
+
export const { plainId, regFrame, ackDiffFrame, heartbeatFrame, listFrame, createdCid, open: openIm } = im;
|
|
31
|
+
const { imId } = im;
|
|
32
|
+
/** create_chat 的 item_id 默认值(上游写死):只给对方用户、不指定商品时用它建会话。 */
|
|
33
|
+
export const DEFAULT_ITEM_ID = '891198795482';
|
|
34
|
+
/** create_chat:按商品和对方建单聊会话(已有时返回原会话)。 */
|
|
35
|
+
export function createChatFrame(myId, toId, itemId = DEFAULT_ITEM_ID) {
|
|
36
|
+
return {
|
|
37
|
+
lwp: '/r/SingleChatConversation/create',
|
|
38
|
+
headers: { mid: generateMid() },
|
|
39
|
+
body: [
|
|
40
|
+
{
|
|
41
|
+
pairFirst: imId(toId),
|
|
42
|
+
pairSecond: imId(myId),
|
|
43
|
+
bizType: '1',
|
|
44
|
+
extension: { itemId },
|
|
45
|
+
ctx: { appVersion: '1.0', platform: 'web' },
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** send_msg:内容是 base64 的 JSON,放在 custom 里(文字 type 1,图片 type 2)。 */
|
|
51
|
+
export function sendMsgFrame(myId, cid, toId, message) {
|
|
52
|
+
const payload = message.type === 'text'
|
|
53
|
+
? { contentType: 1, text: { text: message.text } }
|
|
54
|
+
: { contentType: 2, image: { pics: [{ type: 0, url: message.image_url, width: message.width, height: message.height }] } };
|
|
55
|
+
return {
|
|
56
|
+
lwp: '/r/MessageSend/sendByReceiverScope',
|
|
57
|
+
headers: { mid: generateMid() },
|
|
58
|
+
body: [
|
|
59
|
+
{
|
|
60
|
+
uuid: generateUuid(),
|
|
61
|
+
cid: imId(cid),
|
|
62
|
+
conversationType: 1,
|
|
63
|
+
content: { contentType: 101, custom: { type: payload.contentType, data: Buffer.from(jsonDumps(payload)).toString('base64') } },
|
|
64
|
+
redPointPolicy: 0,
|
|
65
|
+
extension: { extJson: '{}' },
|
|
66
|
+
ctx: { appVersion: '1.0', platform: 'web' },
|
|
67
|
+
mtags: {},
|
|
68
|
+
msgReadStatusSetting: 1,
|
|
69
|
+
},
|
|
70
|
+
{ actualReceivers: [imId(toId), imId(myId)] },
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as n from '../../../core/normalize.js';
|
|
2
|
+
import { jsonLoads } from '../../../core/py.js';
|
|
3
|
+
import { plainId } from './im.js';
|
|
4
|
+
import { itemUrl, userUrl } from './profile.js';
|
|
5
|
+
/** 闲鱼原始对象 → 归一化类型(AGENTS 6.2)。 */
|
|
6
|
+
function ref(id, name) {
|
|
7
|
+
const s = plainId(id);
|
|
8
|
+
return n.userRef({ id: s || null, name, url: s ? userUrl(s) : null });
|
|
9
|
+
}
|
|
10
|
+
/** 商品状态:itemStatus 0 在售,1 已售出,其余下架。 */
|
|
11
|
+
function status(v) {
|
|
12
|
+
const s = v?.itemStatus;
|
|
13
|
+
if (s == null)
|
|
14
|
+
return null;
|
|
15
|
+
return Number(s) === 0 ? 'on_sale' : Number(s) === 1 ? 'sold' : 'off_shelf';
|
|
16
|
+
}
|
|
17
|
+
/** 商品详情(mtop.taobao.idle.pc.detail 的 data:itemDO + sellerDO)。 */
|
|
18
|
+
export function item(d, myId = '') {
|
|
19
|
+
const it = d?.itemDO ?? {};
|
|
20
|
+
const seller = d?.sellerDO ?? {};
|
|
21
|
+
const id = n.id(it.itemId ?? it.id);
|
|
22
|
+
const media = (it.imageInfos ?? []).map((img) => n.media({ type: 'image', url: n.url(img.url) ?? '', width: n.count(img.widthSize), height: n.count(img.heightSize) }, img));
|
|
23
|
+
const title = n.str(it.title);
|
|
24
|
+
const text = n.str(it.desc);
|
|
25
|
+
const video = it.videoPlayInfo?.videoUrl ?? it.videoUrl;
|
|
26
|
+
if (video)
|
|
27
|
+
media.unshift(n.media({ type: 'video', url: n.url(video) }));
|
|
28
|
+
return n.item({
|
|
29
|
+
id,
|
|
30
|
+
kind: 'goods',
|
|
31
|
+
url: itemUrl(id),
|
|
32
|
+
title: title ?? text,
|
|
33
|
+
text,
|
|
34
|
+
author: ref(seller.sellerId ?? it.sellerId, seller.nick ?? seller.uniqueName),
|
|
35
|
+
created_at: n.time(it.gmtCreate),
|
|
36
|
+
cover: media.find((m) => m.type === 'image')?.url ?? null,
|
|
37
|
+
media,
|
|
38
|
+
stats: { views: n.count(it.browseCnt), collects: n.count(it.wantCnt ?? it.collectCnt), comments: null },
|
|
39
|
+
price: n.price(it.soldPrice ?? it.defaultPrice),
|
|
40
|
+
// 只有自己的商品才带状态
|
|
41
|
+
status: myId && String(seller.sellerId ?? it.sellerId) === myId ? status(it) : null,
|
|
42
|
+
}, d);
|
|
43
|
+
}
|
|
44
|
+
/** 当前登录用户(loginuser.get 的 data,缺的字段用 cookie 补)。 */
|
|
45
|
+
export function me(d, fallback) {
|
|
46
|
+
const id = n.id(d?.userId ?? d?.userID ?? d?.uid ?? fallback.id);
|
|
47
|
+
return n.user({
|
|
48
|
+
id,
|
|
49
|
+
name: n.str(d?.nick ?? d?.displayName ?? d?.userNick) ?? fallback.name,
|
|
50
|
+
avatar: n.url(d?.avatar ?? d?.portraitUrl ?? d?.headPic),
|
|
51
|
+
url: userUrl(id),
|
|
52
|
+
}, d);
|
|
53
|
+
}
|
|
54
|
+
/** custom.data:上游发送时是 base64 的 JSON;推送里是 JSON 字符串。 */
|
|
55
|
+
function customData(data) {
|
|
56
|
+
if (data == null || typeof data === 'object')
|
|
57
|
+
return data ?? null;
|
|
58
|
+
const s = String(data);
|
|
59
|
+
for (const text of [s, Buffer.from(s, 'base64').toString('utf8')]) {
|
|
60
|
+
try {
|
|
61
|
+
return jsonLoads(text);
|
|
62
|
+
}
|
|
63
|
+
catch { }
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
/** 消息内容:contentType 1 文字、2 图片,其余(交易卡片等)按卡片处理,文字取摘要。 */
|
|
68
|
+
function body(payload, summary) {
|
|
69
|
+
const type = Number(payload?.contentType);
|
|
70
|
+
if (type === 1)
|
|
71
|
+
return { type: 'text', text: n.str(payload.text?.text) ?? n.str(summary), media: [] };
|
|
72
|
+
if (type === 2) {
|
|
73
|
+
const pics = payload.image?.pics ?? [];
|
|
74
|
+
return {
|
|
75
|
+
type: 'image',
|
|
76
|
+
text: null,
|
|
77
|
+
media: pics.map((p) => n.media({ type: 'image', url: n.url(p.url) ?? '', width: n.count(p.width), height: n.count(p.height) })),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (payload == null)
|
|
81
|
+
return { type: 'other', text: n.str(summary), media: [] };
|
|
82
|
+
return { type: 'card', text: n.str(summary) ?? n.str(payload?.dxCard?.item?.main?.exContent?.title), media: [] };
|
|
83
|
+
}
|
|
84
|
+
/** listUserMessages 返回的一条(userMessageModels[])。 */
|
|
85
|
+
export function historyMessage(model, cid) {
|
|
86
|
+
const m = model?.message ?? {};
|
|
87
|
+
const ext = m.extension ?? {};
|
|
88
|
+
const payload = customData(m.content?.custom?.data);
|
|
89
|
+
return n.message({
|
|
90
|
+
id: n.id(m.messageId ?? m.uuid),
|
|
91
|
+
conversation_id: plainId(m.cid) || cid,
|
|
92
|
+
from: ref(ext.senderUserId ?? m.sender?.uid, ext.reminderTitle),
|
|
93
|
+
...body(payload, ext.reminderContent ?? m.content?.custom?.summary),
|
|
94
|
+
created_at: n.time(m.createAt),
|
|
95
|
+
}, model);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 推送里解码后的消息(上游 handle_message):字段是编号,
|
|
99
|
+
* 1.2 会话、1.3 消息 ID、1.5 时间、1.6.3.5 内容(JSON)、1.10 扩展(reminderTitle 昵称、senderUserId、reminderContent 摘要)。
|
|
100
|
+
* 不是聊天消息的推送(会话状态、已读、正在输入等)返回 null。
|
|
101
|
+
*/
|
|
102
|
+
export function pushMessage(decoded) {
|
|
103
|
+
const m = decoded?.['1'];
|
|
104
|
+
if (!m || typeof m !== 'object' || Array.isArray(m))
|
|
105
|
+
return null;
|
|
106
|
+
const ext = m['10'];
|
|
107
|
+
const cid = m['2'];
|
|
108
|
+
if (!ext || typeof ext !== 'object' || ext.senderUserId == null || typeof cid !== 'string')
|
|
109
|
+
return null;
|
|
110
|
+
const custom = m['6']?.['3'];
|
|
111
|
+
return n.message({
|
|
112
|
+
id: n.id(m['3']),
|
|
113
|
+
conversation_id: plainId(cid),
|
|
114
|
+
from: ref(ext.senderUserId, ext.reminderTitle),
|
|
115
|
+
...body(customData(custom?.['5']), ext.reminderContent ?? custom?.['2']),
|
|
116
|
+
created_at: n.time(m['5']),
|
|
117
|
+
}, decoded);
|
|
118
|
+
}
|
|
119
|
+
/** upload_media 返回的 object:pix 为 `宽x高`。 */
|
|
120
|
+
export function uploaded(obj) {
|
|
121
|
+
const [w, h] = String(obj?.pix ?? '').split('x');
|
|
122
|
+
return n.media({ id: n.idOrNull(obj?.fileId), type: 'image', url: n.url(obj?.url) ?? '', width: n.count(w), height: n.count(h) }, obj);
|
|
123
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 闲鱼 web 端的常量与请求头(上游 goofish_apis.py、goofish_live.py)。
|
|
3
|
+
* 请求头的名字、大小写和顺序照抄上游,对拍测试逐字节比较。
|
|
4
|
+
*/
|
|
5
|
+
/** build_initial_cookies / qrcode_login 用的 UA(Chrome 147)。 */
|
|
6
|
+
export const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36';
|
|
7
|
+
/** XianyuApis 各方法用的 UA(Chrome 146)。 */
|
|
8
|
+
const API_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36';
|
|
9
|
+
/** 私信长连握手用的 UA(Chrome 133,goofish_live.py),照抄。 */
|
|
10
|
+
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';
|
|
11
|
+
const SEC_CH_UA_147 = '"Google Chrome";v="147", "Not.A/Brand";v="8", "Chromium";v="147"';
|
|
12
|
+
const SEC_CH_UA_146 = '"Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146"';
|
|
13
|
+
const ACCEPT_LANGUAGE = 'en,zh-CN;q=0.9,zh;q=0.8,zh-TW;q=0.7,ja;q=0.6';
|
|
14
|
+
export const BROWSER = 'chrome_146';
|
|
15
|
+
export const COOKIE_DOMAIN = '.goofish.com';
|
|
16
|
+
export const H5API = 'https://h5api.m.goofish.com/h5';
|
|
17
|
+
export const PASSPORT = 'https://passport.goofish.com';
|
|
18
|
+
/** mtop 的 appKey,也参与签名(写死在上游 JS 的 generate_sign 里)。 */
|
|
19
|
+
export const APP_KEY = '34839810';
|
|
20
|
+
/** 钉钉 IMPaaS 的 app-key。 */
|
|
21
|
+
export const IM_APP_KEY = '444e9908a51d1cb236a27862abc769c9';
|
|
22
|
+
/** 私信里用户 ID、会话 ID 的后缀。 */
|
|
23
|
+
export const IM_DOMAIN = 'goofish';
|
|
24
|
+
export const WSS_URL = 'wss://wss-goofish.dingtalk.com/';
|
|
25
|
+
export const UPLOAD_URL = 'https://stream-upload.goofish.com/api/upload.api';
|
|
26
|
+
/** /reg 帧里的 ua。 */
|
|
27
|
+
export const IM_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 DingTalk(2.1.5) OS(Windows/10) Browser(Chrome/133.0.0.0) DingWeb/2.1.5 IMPaaS DingWeb/2.1.5';
|
|
28
|
+
/** Python 的 `{**base, **extra}`:已有的键原位改值,新键追加到末尾。 */
|
|
29
|
+
export function merge(base, extra) {
|
|
30
|
+
const out = base.map(([k, v]) => [k, v]);
|
|
31
|
+
for (const [k, v] of extra) {
|
|
32
|
+
const i = out.findIndex(([x]) => x === k);
|
|
33
|
+
if (i >= 0)
|
|
34
|
+
out[i] = [k, v];
|
|
35
|
+
else
|
|
36
|
+
out.push([k, v]);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
/** 真浏览器抓出来的 mtop XHR 请求头(_MTOP_HEADERS)。 */
|
|
41
|
+
export const MTOP_HEADERS = [
|
|
42
|
+
['User-Agent', UA],
|
|
43
|
+
['Accept', 'application/json'],
|
|
44
|
+
['Accept-Language', ACCEPT_LANGUAGE],
|
|
45
|
+
['Accept-Encoding', 'gzip, deflate, br, zstd'],
|
|
46
|
+
['sec-ch-ua', SEC_CH_UA_147],
|
|
47
|
+
['sec-ch-ua-mobile', '?0'],
|
|
48
|
+
['sec-ch-ua-platform', '"Windows"'],
|
|
49
|
+
['Origin', 'https://www.goofish.com'],
|
|
50
|
+
['Referer', 'https://www.goofish.com/'],
|
|
51
|
+
['sec-fetch-dest', 'empty'],
|
|
52
|
+
['sec-fetch-mode', 'cors'],
|
|
53
|
+
['sec-fetch-site', 'same-site'],
|
|
54
|
+
['priority', 'u=1, i'],
|
|
55
|
+
['Content-Type', 'application/x-www-form-urlencoded'],
|
|
56
|
+
];
|
|
57
|
+
/** passport 域的请求头(_PASSPORT_HEADERS)。 */
|
|
58
|
+
export const PASSPORT_HEADERS = [
|
|
59
|
+
['User-Agent', UA],
|
|
60
|
+
['Accept', 'application/json, text/plain, */*'],
|
|
61
|
+
['Accept-Language', ACCEPT_LANGUAGE],
|
|
62
|
+
['Accept-Encoding', 'gzip, deflate, br, zstd'],
|
|
63
|
+
['sec-ch-ua', SEC_CH_UA_147],
|
|
64
|
+
['sec-ch-ua-mobile', '?0'],
|
|
65
|
+
['sec-ch-ua-platform', '"Windows"'],
|
|
66
|
+
['sec-fetch-dest', 'empty'],
|
|
67
|
+
['sec-fetch-mode', 'cors'],
|
|
68
|
+
['sec-fetch-site', 'same-origin'],
|
|
69
|
+
['priority', 'u=1, i'],
|
|
70
|
+
];
|
|
71
|
+
/** build_initial_cookies 的 session 头:只设了 User-Agent。 */
|
|
72
|
+
export const SESSION_HEADERS = [['User-Agent', UA]];
|
|
73
|
+
/** get_token 的请求头。 */
|
|
74
|
+
export const TOKEN_HEADERS = [
|
|
75
|
+
['Host', 'h5api.m.goofish.com'],
|
|
76
|
+
['sec-ch-ua-platform', '"Windows"'],
|
|
77
|
+
['user-agent', API_UA],
|
|
78
|
+
['accept', 'application/json'],
|
|
79
|
+
['sec-ch-ua', SEC_CH_UA_146],
|
|
80
|
+
['content-type', 'application/x-www-form-urlencoded'],
|
|
81
|
+
['sec-ch-ua-mobile', '?0'],
|
|
82
|
+
['origin', 'https://www.goofish.com'],
|
|
83
|
+
['sec-fetch-site', 'same-site'],
|
|
84
|
+
['sec-fetch-mode', 'cors'],
|
|
85
|
+
['sec-fetch-dest', 'empty'],
|
|
86
|
+
['referer', 'https://www.goofish.com/'],
|
|
87
|
+
['accept-language', ACCEPT_LANGUAGE],
|
|
88
|
+
['priority', 'u=1, i'],
|
|
89
|
+
];
|
|
90
|
+
/** refresh_token、get_public_channel、public 共用的请求头。 */
|
|
91
|
+
export const API_HEADERS = [
|
|
92
|
+
['accept', 'application/json'],
|
|
93
|
+
['accept-language', ACCEPT_LANGUAGE],
|
|
94
|
+
['cache-control', 'no-cache'],
|
|
95
|
+
['content-type', 'application/x-www-form-urlencoded'],
|
|
96
|
+
['origin', 'https://www.goofish.com'],
|
|
97
|
+
['pragma', 'no-cache'],
|
|
98
|
+
['priority', 'u=1, i'],
|
|
99
|
+
['referer', 'https://www.goofish.com/'],
|
|
100
|
+
['sec-ch-ua', SEC_CH_UA_146],
|
|
101
|
+
['sec-ch-ua-mobile', '?0'],
|
|
102
|
+
['sec-ch-ua-platform', '"Windows"'],
|
|
103
|
+
['sec-fetch-dest', 'empty'],
|
|
104
|
+
['sec-fetch-mode', 'cors'],
|
|
105
|
+
['sec-fetch-site', 'same-site'],
|
|
106
|
+
['user-agent', API_UA],
|
|
107
|
+
];
|
|
108
|
+
/** get_default_location 的请求头:多一个 eagleeye-userdata。 */
|
|
109
|
+
export const LOCATION_HEADERS = [
|
|
110
|
+
...API_HEADERS.slice(0, 4),
|
|
111
|
+
['eagleeye-userdata', 'spm-cnt=a21ybx'],
|
|
112
|
+
...API_HEADERS.slice(4),
|
|
113
|
+
];
|
|
114
|
+
/** upload_media 的请求头(multipart,content-type 由请求库生成)。 */
|
|
115
|
+
export const UPLOAD_HEADERS = [
|
|
116
|
+
['accept', '*/*'],
|
|
117
|
+
['accept-language', ACCEPT_LANGUAGE],
|
|
118
|
+
['cache-control', 'no-cache'],
|
|
119
|
+
['origin', 'https://www.goofish.com'],
|
|
120
|
+
['pragma', 'no-cache'],
|
|
121
|
+
['priority', 'u=1, i'],
|
|
122
|
+
['referer', 'https://www.goofish.com/'],
|
|
123
|
+
['sec-ch-ua', SEC_CH_UA_146],
|
|
124
|
+
['sec-ch-ua-mobile', '?0'],
|
|
125
|
+
['sec-ch-ua-platform', '"Windows"'],
|
|
126
|
+
['sec-fetch-dest', 'empty'],
|
|
127
|
+
['sec-fetch-mode', 'cors'],
|
|
128
|
+
['sec-fetch-site', 'same-site'],
|
|
129
|
+
['user-agent', API_UA],
|
|
130
|
+
];
|
|
131
|
+
/** 私信长连的握手头(goofish_live.py main / list_all_conversations)。 */
|
|
132
|
+
export function wsHeaders(cookie) {
|
|
133
|
+
return [
|
|
134
|
+
['Cookie', cookie],
|
|
135
|
+
['Host', 'wss-goofish.dingtalk.com'],
|
|
136
|
+
['Connection', 'Upgrade'],
|
|
137
|
+
['Pragma', 'no-cache'],
|
|
138
|
+
['Cache-Control', 'no-cache'],
|
|
139
|
+
['User-Agent', WS_UA],
|
|
140
|
+
['Origin', 'https://www.goofish.com'],
|
|
141
|
+
['Accept-Encoding', 'gzip, deflate, br, zstd'],
|
|
142
|
+
['Accept-Language', 'zh-CN,zh;q=0.9'],
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
export const itemUrl = (id) => `https://www.goofish.com/item?id=${id}`;
|
|
146
|
+
export const userUrl = (id) => `https://www.goofish.com/personal?userId=${id}`;
|