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,557 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { GUEST } from '../../../core/auth-store.js';
|
|
3
|
+
import { SpecificAIError } from '../../../core/errors.js';
|
|
4
|
+
import { writeFileAtomic } from '../../../core/fsutil.js';
|
|
5
|
+
import { cookieCredential, finishLogin, freshCredential, interactive, loginContext, prompt, showQrcode, smsLogin, smsState } from '../../../core/login.js';
|
|
6
|
+
import * as n from '../../../core/normalize.js';
|
|
7
|
+
import { cacheDir } from '../../../core/paths.js';
|
|
8
|
+
import * as rand from '../../../core/rand.js';
|
|
9
|
+
import { reconnecting } from '../../../core/stream.js';
|
|
10
|
+
import { authError, paged } from '../../../core/toolkit.js';
|
|
11
|
+
import * as api from './api.js';
|
|
12
|
+
import { ChatClient, HEARTBEAT_INTERVAL, MsgType, packets, WAITER_APP } from './chat.js';
|
|
13
|
+
import { checkBusiness, checkRisk, Jd } from './client.js';
|
|
14
|
+
import * as login from './login.js';
|
|
15
|
+
import * as norm from './normalize.js';
|
|
16
|
+
import { COOKIE_DOMAIN } from './profile.js';
|
|
17
|
+
import { bootstrapCookies, ITEM_URL_SKU, TraceContext } from './util.js';
|
|
18
|
+
const page = (ctx) => Number(ctx.cursor ?? 1) || 1;
|
|
19
|
+
/** `--area`:收货地区编码,`-` 写法转成接口里的 `_`;没给时由接口从 ipLoc-djd cookie 取。 */
|
|
20
|
+
const areaOpt = (ctx) => ctx.options.area?.replaceAll('-', '_');
|
|
21
|
+
/** 建立会话(web 端没有游客态,core 保证已登录)。 */
|
|
22
|
+
async function session(ctx) {
|
|
23
|
+
return new Jd(ctx);
|
|
24
|
+
}
|
|
25
|
+
/** 需要登录态 cookie 的命令:本地就缺 thor / pin 时直接报错,不发请求。 */
|
|
26
|
+
async function loggedSession(ctx) {
|
|
27
|
+
const jd = await session(ctx);
|
|
28
|
+
jd.requireLogin();
|
|
29
|
+
return jd;
|
|
30
|
+
}
|
|
31
|
+
/** 商品 SKU:纯数字、商品页 URL(PC / 移动 / 全球购),或 3.cn 等短链。 */
|
|
32
|
+
export async function resolveSku(jd, input) {
|
|
33
|
+
const s = String(input ?? '').trim();
|
|
34
|
+
if (/^\d{4,}$/.test(s))
|
|
35
|
+
return s;
|
|
36
|
+
const m = ITEM_URL_SKU.exec(s) ?? /[?&](?:sku|skuId|wareId)=(\d{4,})/.exec(s);
|
|
37
|
+
if (m)
|
|
38
|
+
return m[1];
|
|
39
|
+
if (/^https?:\/\/(3\.cn|u\.jd\.com)\//i.test(s)) {
|
|
40
|
+
let url = s;
|
|
41
|
+
for (let i = 0; i < 5; i++) {
|
|
42
|
+
const res = await jd.http.request({ url, redirect: 'manual', cookies: false });
|
|
43
|
+
const loc = res.headers.get('location');
|
|
44
|
+
if (!loc)
|
|
45
|
+
break;
|
|
46
|
+
url = new URL(loc, url).href;
|
|
47
|
+
const hit = /(\d{4,})\.html/.exec(url);
|
|
48
|
+
if (hit)
|
|
49
|
+
return hit[1];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
throw new SpecificAIError('USAGE', `无法识别的商品:${input}`, { hint: '传商品 SKU(纯数字)或商品页 URL,例如 https://item.jd.com/100012043978.html' });
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 业务响应的通用检查:风控处置(body 里的或 x-rp-content 头里的)、403、登录墙、业务错误。
|
|
56
|
+
* 处置先于 403:带处置的 403 已经说明了原因,不用再探测登录态。
|
|
57
|
+
* strict:正常响应的 code 核对过是 0 / "0" 的接口,其余非 0 的 code 都报 UPSTREAM(见 client.ts 的 checkBusiness)。
|
|
58
|
+
*/
|
|
59
|
+
async function check(jd, res, strict = false) {
|
|
60
|
+
checkRisk(jd, res);
|
|
61
|
+
if (res?._status === 403)
|
|
62
|
+
await diagnose403(jd);
|
|
63
|
+
checkBusiness(jd, res, strict);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 业务接口重试后仍是 403 空 body(上游 JdAPI.diagnose 的第一步):京东对失效的会话不报「未登录」,
|
|
67
|
+
* 而是直接回 403,所以先用免签名的 passport 接口(check_session)探测登录态。
|
|
68
|
+
* 登录已失效 → AUTH_EXPIRED;仍登录 → 被限流或被风控标记(diagnose 的 throttled)。
|
|
69
|
+
* 限流期间不再发签名接口去探测(diagnose 的 getCartNum 探针),免得加重。
|
|
70
|
+
*/
|
|
71
|
+
async function diagnose403(jd) {
|
|
72
|
+
let alive = null;
|
|
73
|
+
try {
|
|
74
|
+
alive = (await api.checkSession(jd)).alive;
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
jd.ctx.log.debug(`登录态探测失败:${err.message}`);
|
|
78
|
+
}
|
|
79
|
+
if (alive === false)
|
|
80
|
+
throw authError(jd.ctx, `账号 ${jd.ctx.account} 的登录态已失效(京东对失效的会话直接回 403)`);
|
|
81
|
+
throw new SpecificAIError('RISK_CONTROL', '京东拒绝了请求(403):登录态有效,被限流或被风控标记了,等十几分钟到几小时再试', {
|
|
82
|
+
detail: { kind: 'rate_limit', status: 403, session: alive ? 'alive' : 'unknown' },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// ================================================================ auth
|
|
86
|
+
function guestLoginContext(ctx, method) {
|
|
87
|
+
return { ...ctx, account: GUEST, credential: freshCredential(ctx, method) };
|
|
88
|
+
}
|
|
89
|
+
/** 新会话:passport 页的埋点 cookie 冷启动(quick.py / login_demo.py 的 bootstrap)。 */
|
|
90
|
+
function loginSession(ctx, method) {
|
|
91
|
+
const jd = new Jd(guestLoginContext(ctx, method));
|
|
92
|
+
jd.update(bootstrapCookies('passport'));
|
|
93
|
+
return jd;
|
|
94
|
+
}
|
|
95
|
+
async function completeLogin(ctx, jd) {
|
|
96
|
+
const s = await api.checkSession(jd);
|
|
97
|
+
if (!s.alive || !jd.pin)
|
|
98
|
+
throw new SpecificAIError('AUTH_REQUIRED', '登录没有成功:登录态校验失败');
|
|
99
|
+
return finishLogin(ctx, jd.ctx.credential, norm.meRef(jd.pin, s.name));
|
|
100
|
+
}
|
|
101
|
+
const QR_POLL = 3000;
|
|
102
|
+
const QR_REFRESH = 100_000;
|
|
103
|
+
const QR_TIMEOUT = 300_000;
|
|
104
|
+
async function qrLogin(ctx) {
|
|
105
|
+
const jd = loginSession(ctx, 'qrcode');
|
|
106
|
+
const { trace } = await login.prepareTrace(jd);
|
|
107
|
+
const deadline = rand.now() + QR_TIMEOUT;
|
|
108
|
+
let nextRefresh = 0;
|
|
109
|
+
let last = null;
|
|
110
|
+
while (rand.now() < deadline) {
|
|
111
|
+
if (rand.now() >= nextRefresh) {
|
|
112
|
+
const { png, content } = await login.getQrcode(jd);
|
|
113
|
+
if (content)
|
|
114
|
+
await showQrcode(ctx, content, '请用京东 App 扫码并确认');
|
|
115
|
+
else {
|
|
116
|
+
// 没有 QRCodeKey、解不出二维码内容时只能给原图(与 showQrcode 一样原子写入、0600)
|
|
117
|
+
const file = join(cacheDir('jd'), 'qrcode.png');
|
|
118
|
+
await writeFileAtomic(file, png);
|
|
119
|
+
ctx.log.info(`请用京东 App 扫描二维码并确认:${file}`);
|
|
120
|
+
}
|
|
121
|
+
nextRefresh = rand.now() + QR_REFRESH;
|
|
122
|
+
last = null;
|
|
123
|
+
}
|
|
124
|
+
await rand.sleep(QR_POLL);
|
|
125
|
+
const res = await login.checkQrcode(jd);
|
|
126
|
+
if (res.code !== last) {
|
|
127
|
+
if (res.code === 202)
|
|
128
|
+
ctx.log.info('已扫码,请在手机上确认');
|
|
129
|
+
else if (res.code === 203 || res.code === 205)
|
|
130
|
+
nextRefresh = 0;
|
|
131
|
+
last = res.code;
|
|
132
|
+
}
|
|
133
|
+
if (res.code === 200 && res.ticket) {
|
|
134
|
+
await login.validateTicket(jd, String(res.ticket), trace);
|
|
135
|
+
return completeLogin(ctx, jd);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
throw new SpecificAIError('AUTH_REQUIRED', `${QR_TIMEOUT / 1000} 秒内没有完成扫码`, { hint: '重新执行 specific-ai jd auth login' });
|
|
139
|
+
}
|
|
140
|
+
/** 短信登录的中间态:会话 cookie、设备数据、登录页隐藏域与链路上下文。 */
|
|
141
|
+
function saveSms(jd, sms, mobile, extra = {}) {
|
|
142
|
+
return {
|
|
143
|
+
stage: 'sms',
|
|
144
|
+
mobile,
|
|
145
|
+
cookies: structuredClone(jd.ctx.credential.scopes.main.cookies),
|
|
146
|
+
device: structuredClone(jd.ctx.credential.device),
|
|
147
|
+
fields: sms.fields,
|
|
148
|
+
publicKey: sms.publicKey,
|
|
149
|
+
pageId: sms.trace.pageId,
|
|
150
|
+
sessionId: sms.trace.sessionId,
|
|
151
|
+
...extra,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function restoreSms(ctx, state) {
|
|
155
|
+
const lctx = guestLoginContext(ctx, 'sms');
|
|
156
|
+
lctx.credential.scopes.main.cookies = structuredClone(state.cookies);
|
|
157
|
+
lctx.credential.device = structuredClone(state.device);
|
|
158
|
+
const jd = new Jd(lctx);
|
|
159
|
+
const sms = {
|
|
160
|
+
trace: new TraceContext(state.pageId, state.sessionId),
|
|
161
|
+
fields: state.fields,
|
|
162
|
+
publicKey: state.publicKey,
|
|
163
|
+
captchaStatus: 0,
|
|
164
|
+
captchaSessionId: '',
|
|
165
|
+
captchaJwtToken: '',
|
|
166
|
+
};
|
|
167
|
+
return { jd, sms };
|
|
168
|
+
}
|
|
169
|
+
/** 额外安全验证:发第二条短信;TTY 下直接输入,否则保存中间态,下一次 --code 继续。 */
|
|
170
|
+
async function safeVerify(ctx, jd, sms, mobile, payload) {
|
|
171
|
+
const page = await login.loadSafeVerify(jd, payload);
|
|
172
|
+
const sent = await login.sendSafeCode(jd, page);
|
|
173
|
+
if (!sent.success)
|
|
174
|
+
throw new SpecificAIError('UPSTREAM', sent.message);
|
|
175
|
+
ctx.log.info(sent.message);
|
|
176
|
+
if (!interactive()) {
|
|
177
|
+
await smsState.save(ctx, saveSms(jd, sms, mobile, { stage: 'safe', page, method: sent.method }));
|
|
178
|
+
throw new SpecificAIError('AUTH_REQUIRED', '账号需要额外安全验证,验证码已发送', {
|
|
179
|
+
hint: `收到后执行:specific-ai jd auth login --method sms${ctx.account ? ` -a ${ctx.account}` : ''} --code <验证码>`,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
const code = (await prompt('额外安全验证短信验证码(6位):')).trim();
|
|
183
|
+
return finishSafe(ctx, jd, page, sent.method, code);
|
|
184
|
+
}
|
|
185
|
+
async function finishSafe(ctx, jd, page, method, code) {
|
|
186
|
+
const r = await login.submitSafeCode(jd, page, method, code);
|
|
187
|
+
if (!r.success)
|
|
188
|
+
throw new SpecificAIError('UPSTREAM', r.message);
|
|
189
|
+
if (!jd.isLogin)
|
|
190
|
+
throw new SpecificAIError('AUTH_REQUIRED', '额外安全验证通过,但未取得 thor/pin');
|
|
191
|
+
return completeLogin(ctx, jd);
|
|
192
|
+
}
|
|
193
|
+
async function smsFlow(ctx) {
|
|
194
|
+
return smsLogin(ctx, {
|
|
195
|
+
send: async (phone) => {
|
|
196
|
+
const mobile = login.loginMobile(phone);
|
|
197
|
+
const jd = loginSession(ctx, 'sms');
|
|
198
|
+
ctx.log.info('正在初始化手机号登录');
|
|
199
|
+
const sms = await login.startSms(jd);
|
|
200
|
+
let token = '';
|
|
201
|
+
if (sms.captchaStatus === 1) {
|
|
202
|
+
ctx.log.info('正在纯程序计算人机验证');
|
|
203
|
+
token = await login.solveSmsCaptcha(jd, sms, mobile);
|
|
204
|
+
}
|
|
205
|
+
const r = await login.sendCode(jd, sms, mobile, token);
|
|
206
|
+
if (!r.success)
|
|
207
|
+
throw new SpecificAIError('UPSTREAM', r.message, { detail: { code: r.payload?.code ?? null } });
|
|
208
|
+
ctx.log.info(r.message);
|
|
209
|
+
return saveSms(jd, sms, mobile);
|
|
210
|
+
},
|
|
211
|
+
verify: async (state, code) => {
|
|
212
|
+
const { jd, sms } = restoreSms(ctx, state);
|
|
213
|
+
if (state.stage === 'safe')
|
|
214
|
+
return finishSafe(ctx, jd, state.page, state.method, code);
|
|
215
|
+
const r = await login.submitCode(jd, sms, state.mobile, code.trim());
|
|
216
|
+
if (r.success)
|
|
217
|
+
return completeLogin(ctx, jd);
|
|
218
|
+
if (r.payload?.newSafeVerify)
|
|
219
|
+
return safeVerify(ctx, jd, sms, state.mobile, r.payload);
|
|
220
|
+
throw new SpecificAIError('UPSTREAM', r.message);
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
export async function authLogin(ctx) {
|
|
225
|
+
const method = ctx.options.method;
|
|
226
|
+
if (method === 'cookie') {
|
|
227
|
+
const imported = cookieCredential(ctx, COOKIE_DOMAIN);
|
|
228
|
+
const lctx = loginContext(ctx, freshCredential(ctx, 'cookie'));
|
|
229
|
+
const jd = new Jd(lctx);
|
|
230
|
+
jd.update(imported.scopes.main.cookies.map((c) => [c.name, c.value]));
|
|
231
|
+
if (!jd.isLogin)
|
|
232
|
+
throw new SpecificAIError('USAGE', 'cookie 里缺少 thor(或 pt_key)与 pin', { hint: '从已登录的 www.jd.com 复制完整的 Cookie' });
|
|
233
|
+
return completeLogin(ctx, jd);
|
|
234
|
+
}
|
|
235
|
+
if (method === 'sms')
|
|
236
|
+
return smsFlow(ctx);
|
|
237
|
+
if (method === 'qrcode')
|
|
238
|
+
return qrLogin(ctx);
|
|
239
|
+
throw new SpecificAIError('USAGE', `不支持的登录方式:${method}`);
|
|
240
|
+
}
|
|
241
|
+
export async function authStatus(ctx) {
|
|
242
|
+
const jd = new Jd(ctx);
|
|
243
|
+
if (!jd.isLogin)
|
|
244
|
+
return { logged_in: false, user: null, method: null, expires_at: null };
|
|
245
|
+
const s = await api.checkSession(jd);
|
|
246
|
+
return {
|
|
247
|
+
logged_in: s.alive,
|
|
248
|
+
user: s.alive && jd.pin ? norm.meRef(jd.pin, s.name) : null,
|
|
249
|
+
method: s.alive ? ctx.credential.method : null,
|
|
250
|
+
expires_at: null,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
// ================================================================ user
|
|
254
|
+
export async function userGet(ctx) {
|
|
255
|
+
const jd = await session(ctx);
|
|
256
|
+
const who = ctx.args.user ?? 'me';
|
|
257
|
+
if (who !== 'me' && who !== jd.pin)
|
|
258
|
+
throw new SpecificAIError('UNSUPPORTED', 'jd 的 user get 只支持 me', { hint: 'specific-ai jd user get me' });
|
|
259
|
+
if (!jd.isLogin)
|
|
260
|
+
throw authError(ctx);
|
|
261
|
+
const s = await api.checkSession(jd);
|
|
262
|
+
if (!s.alive)
|
|
263
|
+
throw authError(ctx);
|
|
264
|
+
return norm.me(jd.pin, s.data);
|
|
265
|
+
}
|
|
266
|
+
const PAGE_SIZE = 20;
|
|
267
|
+
export async function userCollects(ctx) {
|
|
268
|
+
const who = ctx.args.user ?? 'me';
|
|
269
|
+
const jd = await loggedSession(ctx);
|
|
270
|
+
if (who !== 'me' && who !== jd.pin)
|
|
271
|
+
throw new SpecificAIError('UNSUPPORTED', 'jd 只能查看自己关注的商品', { hint: 'specific-ai jd user collects' });
|
|
272
|
+
const p = page(ctx);
|
|
273
|
+
const d = await api.followProducts(jd, p, PAGE_SIZE, areaOpt(ctx));
|
|
274
|
+
await check(jd, d);
|
|
275
|
+
const list = norm.listedItems(d);
|
|
276
|
+
return paged(list, p + 1, list.length >= PAGE_SIZE);
|
|
277
|
+
}
|
|
278
|
+
// ================================================================ item
|
|
279
|
+
export async function itemGet(ctx) {
|
|
280
|
+
const jd = await session(ctx);
|
|
281
|
+
const sku = await resolveSku(jd, ctx.args.item);
|
|
282
|
+
const d = await api.productDetail(jd, sku, areaOpt(ctx));
|
|
283
|
+
await check(jd, d);
|
|
284
|
+
return norm.detail(sku, d);
|
|
285
|
+
}
|
|
286
|
+
/** specific-ai 的 --sort → 上游 search 的 sort 取值。 */
|
|
287
|
+
const SORT = {
|
|
288
|
+
general: '',
|
|
289
|
+
sales: 'sort_totalsales15_desc',
|
|
290
|
+
price_asc: 'sort_price_asc',
|
|
291
|
+
price_desc: 'sort_price_desc',
|
|
292
|
+
comments: 'sort_commentcount_desc',
|
|
293
|
+
};
|
|
294
|
+
async function searchItems(ctx, jd, keyword, sort = '') {
|
|
295
|
+
const p = page(ctx);
|
|
296
|
+
const res = await api.search(jd, keyword, p, { sort, area: areaOpt(ctx) });
|
|
297
|
+
if (res?._verification && res._verification.code !== 0) {
|
|
298
|
+
throw new SpecificAIError('RISK_CONTROL', '京东要求人机验证,纯程序验证没有通过', { hint: '稍后重试,或登录后再搜索', detail: { kind: 'captcha', ...res._verification } });
|
|
299
|
+
}
|
|
300
|
+
await check(jd, res);
|
|
301
|
+
const data = res?.data ?? {};
|
|
302
|
+
const list = (Array.isArray(data.wareList) ? data.wareList : []).map(norm.ware);
|
|
303
|
+
const total = Number(data.resultCount ?? 0);
|
|
304
|
+
return paged(list, p + 1, list.length > 0 && p * 30 < total);
|
|
305
|
+
}
|
|
306
|
+
export async function itemSearch(ctx) {
|
|
307
|
+
const jd = await session(ctx);
|
|
308
|
+
return searchItems(ctx, jd, ctx.args.keyword, SORT[ctx.options.sort ?? 'general'] ?? '');
|
|
309
|
+
}
|
|
310
|
+
/** 相关推荐:上游只有商品页的相关搜索词(relsearch),取第一个相关词搜出的商品。 */
|
|
311
|
+
export async function itemRelated(ctx) {
|
|
312
|
+
const jd = await session(ctx);
|
|
313
|
+
const sku = await resolveSku(jd, ctx.args.item);
|
|
314
|
+
const rel = await api.relatedSearch(jd, sku);
|
|
315
|
+
await check(jd, rel);
|
|
316
|
+
const keyword = norm.keywordsFromRel(rel)[0]?.text;
|
|
317
|
+
if (!keyword)
|
|
318
|
+
return paged([], null, false);
|
|
319
|
+
return searchItems(ctx, jd, keyword);
|
|
320
|
+
}
|
|
321
|
+
// ================================================================ comment
|
|
322
|
+
/** 商品页一次取的评价条数;`--limit N` 映射到接口的 commentNum,一次请求取 N 条。 */
|
|
323
|
+
const COMMENT_NUM = 10;
|
|
324
|
+
export async function commentList(ctx) {
|
|
325
|
+
const jd = await session(ctx);
|
|
326
|
+
const sku = await resolveSku(jd, ctx.args.item);
|
|
327
|
+
const d = await api.productComments(jd, sku, ctx.options.limit ?? COMMENT_NUM);
|
|
328
|
+
await check(jd, d);
|
|
329
|
+
return paged(norm.comments(sku, d), null, false);
|
|
330
|
+
}
|
|
331
|
+
// ================================================================ keyword
|
|
332
|
+
export async function keywordSuggest(ctx) {
|
|
333
|
+
const jd = await session(ctx);
|
|
334
|
+
const d = await api.searchRelwords(jd, ctx.args.prefix);
|
|
335
|
+
await check(jd, d);
|
|
336
|
+
return norm.keywordsFromRel(d);
|
|
337
|
+
}
|
|
338
|
+
export async function keywordHot(ctx) {
|
|
339
|
+
const jd = await session(ctx);
|
|
340
|
+
const d = await api.searchHotwords(jd);
|
|
341
|
+
await check(jd, d, true);
|
|
342
|
+
return norm.keywordsFromHot(d);
|
|
343
|
+
}
|
|
344
|
+
// ================================================================ history、订单、购物车、优惠券
|
|
345
|
+
export async function historyList(ctx) {
|
|
346
|
+
const jd = await loggedSession(ctx);
|
|
347
|
+
const p = page(ctx);
|
|
348
|
+
const d = await api.browseHistory(jd, p, PAGE_SIZE, areaOpt(ctx));
|
|
349
|
+
await check(jd, d);
|
|
350
|
+
const list = norm.listedItems(d);
|
|
351
|
+
return paged(list, p + 1, list.length >= PAGE_SIZE);
|
|
352
|
+
}
|
|
353
|
+
/** `--range` → 订单中心下拉框的 `d`(get_order_list 的 date_range):1 近三个月、2 今年内、四位年份为那一年。 */
|
|
354
|
+
export function orderRange(range) {
|
|
355
|
+
if (!range || range === '3m')
|
|
356
|
+
return '1';
|
|
357
|
+
if (range === 'this_year')
|
|
358
|
+
return '2';
|
|
359
|
+
return range;
|
|
360
|
+
}
|
|
361
|
+
export async function orderList(ctx) {
|
|
362
|
+
const jd = await loggedSession(ctx);
|
|
363
|
+
const p = page(ctx);
|
|
364
|
+
const res = await api.orderList(jd, p, orderRange(ctx.options.range));
|
|
365
|
+
if (res._error)
|
|
366
|
+
throw authError(ctx, res._error);
|
|
367
|
+
return paged(res.orders.map((o) => norm.order(o)), p + 1, res.orders.length > 0);
|
|
368
|
+
}
|
|
369
|
+
export async function cartCount(ctx) {
|
|
370
|
+
const jd = await loggedSession(ctx);
|
|
371
|
+
const d = await api.cartNum(jd, areaOpt(ctx));
|
|
372
|
+
await check(jd, d);
|
|
373
|
+
const count = norm.cartCount(d);
|
|
374
|
+
if (count == null)
|
|
375
|
+
throw new SpecificAIError('UPSTREAM', '购物车接口没有返回数量', { detail: { code: d?.code ?? null } });
|
|
376
|
+
return { count };
|
|
377
|
+
}
|
|
378
|
+
export async function couponList(ctx) {
|
|
379
|
+
const jd = await session(ctx);
|
|
380
|
+
const sku = await resolveSku(jd, ctx.args.item);
|
|
381
|
+
const d = await api.recommendCoupon(jd, sku, areaOpt(ctx));
|
|
382
|
+
await check(jd, d);
|
|
383
|
+
return norm.coupons(d);
|
|
384
|
+
}
|
|
385
|
+
// ================================================================ msg(咚咚客服)
|
|
386
|
+
/** 咚咚会话需要 aid:没有时先 getAidInfo。 */
|
|
387
|
+
async function chatSession(ctx) {
|
|
388
|
+
const jd = await loggedSession(ctx);
|
|
389
|
+
if (!jd.chat.aid) {
|
|
390
|
+
const r = await api.aidInfo(jd);
|
|
391
|
+
await check(jd, r, true);
|
|
392
|
+
if (!r?.aid)
|
|
393
|
+
throw new SpecificAIError('UPSTREAM', '咚咚没有下发 aid', { detail: { code: r?.code ?? null } });
|
|
394
|
+
}
|
|
395
|
+
return jd;
|
|
396
|
+
}
|
|
397
|
+
export async function msgList(ctx) {
|
|
398
|
+
const jd = await chatSession(ctx);
|
|
399
|
+
const d = await api.chatSessionLog(jd);
|
|
400
|
+
await check(jd, d, true);
|
|
401
|
+
return paged(norm.conversations(d), null, false);
|
|
402
|
+
}
|
|
403
|
+
const HISTORY_SIZE = 20;
|
|
404
|
+
export async function msgHistory(ctx) {
|
|
405
|
+
const jd = await chatSession(ctx);
|
|
406
|
+
const venderId = ctx.args.conversation;
|
|
407
|
+
const before = Number(ctx.cursor ?? 0) || 0;
|
|
408
|
+
const d = await api.queryLastLogs(jd, venderId, HISTORY_SIZE, before);
|
|
409
|
+
await check(jd, d);
|
|
410
|
+
const raw = norm.findList(d, ['body', 'mid', 'from']);
|
|
411
|
+
// 游标是这一页最早一条的毫秒时间戳(queryLastLogs 的 startTimeStamp)。created_at 只到秒,拿它还原会跳过同一秒里的其余消息,
|
|
412
|
+
// 所以取原始的毫秒 timestamp;没有时才退回 datetime
|
|
413
|
+
const oldest = raw.reduce((min, m) => {
|
|
414
|
+
const t = Number(m?.timestamp ?? m?.time) || Date.parse(n.time(m?.datetime) ?? '');
|
|
415
|
+
return Number.isFinite(t) && t > 0 && (min == null || t < min) ? t : min;
|
|
416
|
+
}, null);
|
|
417
|
+
return paged(raw.map((m) => norm.message(m, venderId)), oldest, raw.length >= HISTORY_SIZE && oldest != null);
|
|
418
|
+
}
|
|
419
|
+
/** 商家侧 appId:咚咚消息信封的 to.app(get_vender_app),京东自营是 jd.waiter。 */
|
|
420
|
+
async function venderApp(jd, venderId, pid = '', orderId = '') {
|
|
421
|
+
const r = await api.chatInfo(jd, venderId, pid, orderId);
|
|
422
|
+
return String(r?.body?.cache?.vender?.appId || WAITER_APP);
|
|
423
|
+
}
|
|
424
|
+
/** 京东自营客服的 venderId(上游 get_chat_info / JdChatWS 的默认值)。 */
|
|
425
|
+
const SELF_VENDER = '1';
|
|
426
|
+
/** 订单号:纯数字(order list 输出的 id),或订单详情页 URL 里的 orderid。 */
|
|
427
|
+
export function resolveOrderId(input) {
|
|
428
|
+
const s = String(input ?? '').trim();
|
|
429
|
+
if (/^\d{6,}$/.test(s))
|
|
430
|
+
return s;
|
|
431
|
+
const m = /[?&]orderid=(\d{6,})/i.exec(s);
|
|
432
|
+
if (m)
|
|
433
|
+
return m[1];
|
|
434
|
+
throw new SpecificAIError('USAGE', `无法识别的订单:${input}`, { hint: '传订单号(specific-ai jd order list 输出的 id),或订单详情页 URL' });
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* 咨询对象(chat_demo.py 的 VENDER_ID / SKU_ID,加上 get_chat_info 的 order_id):
|
|
438
|
+
* --conversation 直接给商家 venderId;--item 从商品详情取 venderId;只有 --order 时是京东自营客服。
|
|
439
|
+
*/
|
|
440
|
+
export async function chatTarget(ctx, jd) {
|
|
441
|
+
const o = ctx.options;
|
|
442
|
+
const orderId = o.order != null ? resolveOrderId(o.order) : '';
|
|
443
|
+
let venderId = o.conversation ?? SELF_VENDER;
|
|
444
|
+
let pid = '';
|
|
445
|
+
if (o.item) {
|
|
446
|
+
pid = await resolveSku(jd, o.item);
|
|
447
|
+
const d = await api.productDetail(jd, pid);
|
|
448
|
+
await check(jd, d);
|
|
449
|
+
venderId = norm.detailVenderId(d) ?? '';
|
|
450
|
+
if (!venderId)
|
|
451
|
+
throw new SpecificAIError('UPSTREAM', '商品详情里没有商家 venderId', { hint: '用 --conversation <商家 venderId> 直接指定商家' });
|
|
452
|
+
}
|
|
453
|
+
return { venderId, pid, orderId };
|
|
454
|
+
}
|
|
455
|
+
export async function msgSend(ctx) {
|
|
456
|
+
const o = ctx.options;
|
|
457
|
+
if (o.to)
|
|
458
|
+
throw new SpecificAIError('UNSUPPORTED', '京东咚咚只能联系商家客服', { hint: '用 --item <商品>、--conversation <商家 venderId> 或 --order <订单>' });
|
|
459
|
+
if (o.image || o.video)
|
|
460
|
+
throw new SpecificAIError('UNSUPPORTED', '京东咚咚暂只支持发文本');
|
|
461
|
+
const text = ctx.args.text;
|
|
462
|
+
if (!text)
|
|
463
|
+
throw new SpecificAIError('USAGE', '需要消息内容');
|
|
464
|
+
if (o.order != null)
|
|
465
|
+
resolveOrderId(o.order);
|
|
466
|
+
const jd = await chatSession(ctx);
|
|
467
|
+
const { venderId, pid, orderId } = await chatTarget(ctx, jd);
|
|
468
|
+
const app = await venderApp(jd, venderId, pid, orderId);
|
|
469
|
+
const chat = new ChatClient(jd, venderId, app);
|
|
470
|
+
const socket = await chat.connect(ctx.signal);
|
|
471
|
+
try {
|
|
472
|
+
await chat.send(chat.heartbeatPacket());
|
|
473
|
+
await chat.send(chat.helloPacket(pid, orderId));
|
|
474
|
+
const packet = chat.textPacket(text, pid, orderId);
|
|
475
|
+
await chat.send(packet);
|
|
476
|
+
await waitReceipt(ctx, socket.messages, String(packet.id));
|
|
477
|
+
return norm.message(packet, venderId);
|
|
478
|
+
}
|
|
479
|
+
finally {
|
|
480
|
+
chat.close();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/** 等服务端回执的时长。 */
|
|
484
|
+
const RECEIPT_TIMEOUT = 5000;
|
|
485
|
+
/**
|
|
486
|
+
* 等这条消息的回执:按帧的 id 对上刚发的消息(欢迎语、心跳也有各自的回执,不能拿来算)。
|
|
487
|
+
* - 对上的 chat_message_result → 发送成功;对上的 failure → UPSTREAM;
|
|
488
|
+
* - 超时:途中收到过没对上 id 的 failure 时按失败报 UPSTREAM,否则只在 stderr 提示没等到回执(消息可能已送达)。
|
|
489
|
+
*/
|
|
490
|
+
export async function waitReceipt(ctx, messages, id, timeout = RECEIPT_TIMEOUT) {
|
|
491
|
+
const it = messages[Symbol.asyncIterator]();
|
|
492
|
+
const deadline = rand.now() + timeout;
|
|
493
|
+
let timer;
|
|
494
|
+
let pending = null;
|
|
495
|
+
let other = null;
|
|
496
|
+
const fail = (p) => new SpecificAIError('UPSTREAM', `咚咚发送失败:${p.body?.msg ?? p.body?.code ?? ''}`, { detail: p.body ?? null });
|
|
497
|
+
try {
|
|
498
|
+
while (rand.now() < deadline) {
|
|
499
|
+
pending ??= it.next();
|
|
500
|
+
const expired = new Promise((r) => (timer = setTimeout(() => r(null), Math.max(0, deadline - rand.now()))));
|
|
501
|
+
const next = await Promise.race([pending, expired]);
|
|
502
|
+
clearTimeout(timer);
|
|
503
|
+
if (!next)
|
|
504
|
+
break;
|
|
505
|
+
pending = null;
|
|
506
|
+
if (next.done)
|
|
507
|
+
break;
|
|
508
|
+
for (const p of packets(next.value)) {
|
|
509
|
+
if (p?.type === MsgType.CHAT_MESSAGE_RESULT && p.id === id)
|
|
510
|
+
return;
|
|
511
|
+
if (p?.type === MsgType.FAILURE) {
|
|
512
|
+
if (p.id === id)
|
|
513
|
+
throw fail(p);
|
|
514
|
+
other ??= p;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
finally {
|
|
520
|
+
clearTimeout(timer);
|
|
521
|
+
// 超时时还挂着的 next() 与关闭迭代器:连接随后由 chat.close() 关掉,它们的结果(包括出错)都不再需要
|
|
522
|
+
pending?.catch(() => { });
|
|
523
|
+
it.return?.()?.catch(() => { });
|
|
524
|
+
}
|
|
525
|
+
if (other)
|
|
526
|
+
throw fail(other);
|
|
527
|
+
ctx.log.warn(`咚咚 ${timeout / 1000} 秒内没有回执,消息可能没有送达`);
|
|
528
|
+
}
|
|
529
|
+
/** 监听咚咚消息:只发心跳,不像上游示例那样自动向京东自营发起咨询。 */
|
|
530
|
+
export function msgListen(ctx) {
|
|
531
|
+
return (async function* () {
|
|
532
|
+
const jd = await chatSession(ctx);
|
|
533
|
+
yield* reconnecting(ctx, async function* () {
|
|
534
|
+
const chat = new ChatClient(jd);
|
|
535
|
+
const socket = await chat.connect(ctx.signal);
|
|
536
|
+
const beat = () => chat.send(chat.heartbeatPacket()).catch(() => { });
|
|
537
|
+
void beat();
|
|
538
|
+
const timer = setInterval(beat, HEARTBEAT_INTERVAL).unref();
|
|
539
|
+
try {
|
|
540
|
+
for await (const raw of socket.messages) {
|
|
541
|
+
for (const p of packets(raw)) {
|
|
542
|
+
ctx.log.debug(`ws ${p?.type}`);
|
|
543
|
+
if (p?.type === MsgType.FAILURE)
|
|
544
|
+
ctx.log.warn(`咚咚错误 code=${p.body?.code} ${p.body?.msg ?? ''}`);
|
|
545
|
+
const m = norm.chatEvent(p);
|
|
546
|
+
if (m)
|
|
547
|
+
yield m;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
finally {
|
|
552
|
+
clearInterval(timer);
|
|
553
|
+
chat.close();
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
})();
|
|
557
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { disableEnvProxy, HttpClient } from '../../../../core/http.js';
|
|
3
|
+
import { BROWSER } from '../profile.js';
|
|
4
|
+
import { imdecode } from '../../../../core/image.js';
|
|
5
|
+
import { solveClick, solveRotation, solveSlider, solveTrace } from './solver.js';
|
|
6
|
+
/**
|
|
7
|
+
* JCAP 运行时的子进程入口(由 host.js 代替上游的 Python 脚本):
|
|
8
|
+
* `helper.js bridge`:上游 static/jcap/env/http_bridge.py,stdin 一个请求,stdout 一个响应(JSON)。
|
|
9
|
+
* `helper.js solve --tp N ...`:上游 static/jcap/run/captcha_solver.py,stdin 图片字节,stdout 解(JSON)。
|
|
10
|
+
*/
|
|
11
|
+
async function bridge() {
|
|
12
|
+
disableEnvProxy();
|
|
13
|
+
const req = JSON.parse(readFileSync(0, 'utf8'));
|
|
14
|
+
const method = String(req.method || 'GET').toUpperCase();
|
|
15
|
+
const http = new HttpClient({ browser: BROWSER, os: 'windows', proxy: process.env.SPECIFIC_AI_JD_PROXY || null, timeout: 20 });
|
|
16
|
+
const headers = Object.entries((req.headers ?? {})).map(([k, v]) => [k, String(v)]);
|
|
17
|
+
const res = await http.request({ method, url: String(req.url), headers, body: method === 'GET' ? undefined : String(req.body ?? '') });
|
|
18
|
+
const raw = new Uint8Array(await res.arrayBuffer());
|
|
19
|
+
let body;
|
|
20
|
+
try {
|
|
21
|
+
body = new TextDecoder('utf-8', { fatal: true }).decode(raw);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
body = new TextDecoder('gb18030').decode(raw);
|
|
25
|
+
}
|
|
26
|
+
const pairs = [];
|
|
27
|
+
res.headers.forEach((v, k) => {
|
|
28
|
+
if (k !== 'set-cookie')
|
|
29
|
+
pairs.push([k, v]);
|
|
30
|
+
});
|
|
31
|
+
const setCookies = res.headers.getSetCookie();
|
|
32
|
+
for (const c of setCookies)
|
|
33
|
+
pairs.push(['set-cookie', c]);
|
|
34
|
+
return JSON.stringify({ status: res.status, statusText: '', url: res.url || String(req.url), headers: pairs, setCookies, body });
|
|
35
|
+
}
|
|
36
|
+
function options(argv) {
|
|
37
|
+
const out = {};
|
|
38
|
+
for (let i = 0; i < argv.length; i += 2) {
|
|
39
|
+
const k = argv[i];
|
|
40
|
+
if (!k.startsWith('--') || i + 1 >= argv.length)
|
|
41
|
+
throw new Error('invalid solver arguments');
|
|
42
|
+
out[k.slice(2)] = argv[i + 1];
|
|
43
|
+
}
|
|
44
|
+
if (!('tp' in out))
|
|
45
|
+
throw new Error('missing challenge type');
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
async function solve(argv) {
|
|
49
|
+
const o = options(argv);
|
|
50
|
+
const tp = Number.parseInt(o.tp, 10);
|
|
51
|
+
const image = await imdecode(readFileSync(0), 'color');
|
|
52
|
+
if (!image)
|
|
53
|
+
throw new Error('invalid challenge image');
|
|
54
|
+
let solution;
|
|
55
|
+
if (tp === 2 || tp === 3) {
|
|
56
|
+
const model = o.model;
|
|
57
|
+
if (!model)
|
|
58
|
+
throw new Error('missing saliency model');
|
|
59
|
+
if (tp === 2) {
|
|
60
|
+
const tip = await imdecode(Buffer.from(o['tip-base64'] ?? '', 'base64'), 'color');
|
|
61
|
+
if (!tip)
|
|
62
|
+
throw new Error('invalid tip image');
|
|
63
|
+
solution = await solveClick(image, tip, model);
|
|
64
|
+
}
|
|
65
|
+
else
|
|
66
|
+
solution = await solveTrace(image, model);
|
|
67
|
+
}
|
|
68
|
+
else if (tp === 26) {
|
|
69
|
+
const model = o['orientation-model'];
|
|
70
|
+
if (!model)
|
|
71
|
+
throw new Error('missing orientation model');
|
|
72
|
+
solution = await solveRotation(image, model);
|
|
73
|
+
}
|
|
74
|
+
else if (tp === 30) {
|
|
75
|
+
const slot = await imdecode(Buffer.from(o['slot-base64'] ?? '', 'base64'), 'unchanged');
|
|
76
|
+
if (!slot)
|
|
77
|
+
throw new Error('invalid slot image');
|
|
78
|
+
solution = await solveSlider(image, slot);
|
|
79
|
+
}
|
|
80
|
+
else
|
|
81
|
+
throw new Error(`unsupported challenge type: ${tp}`);
|
|
82
|
+
return JSON.stringify(solution) + '\n';
|
|
83
|
+
}
|
|
84
|
+
const [mode, ...rest] = process.argv.slice(2);
|
|
85
|
+
const run = mode === 'bridge' ? bridge() : mode === 'solve' ? solve(rest) : Promise.reject(new Error(`unknown mode ${mode}`));
|
|
86
|
+
// 管道写在 POSIX 上是异步的:写完再退出
|
|
87
|
+
run.then((out) => process.stdout.write(out, () => process.exit(0)), (err) => {
|
|
88
|
+
process.stderr.write(String(err?.message ?? err) + '\n');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
});
|