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,191 @@
|
|
|
1
|
+
import { readFile, rm } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
import { text as readStreamText } from 'node:stream/consumers';
|
|
5
|
+
import QRCode from 'qrcode';
|
|
6
|
+
import { endpointFlag, getCurrent, newCredential, readCredential, setCurrent, toAccount, writeCredential } from './auth-store.js';
|
|
7
|
+
import { parseCookieInput } from './cookies.js';
|
|
8
|
+
import { SpecificAIError } from './errors.js';
|
|
9
|
+
import { writeFileAtomic } from './fsutil.js';
|
|
10
|
+
import { cacheDir } from './paths.js';
|
|
11
|
+
import { now, sleep } from './rand.js';
|
|
12
|
+
/** 登录流程的公共部分(AGENTS 5.3)。各平台的 `auth login` handler 调用这些函数。 */
|
|
13
|
+
/** 登录到哪个账号:`-a` > 当前账号 > `default`。 */
|
|
14
|
+
export function loginTarget(ctx) {
|
|
15
|
+
return ctx.account ?? 'default';
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 登录流程里校验新凭证用的上下文:账号是要登录到的那个(出错时提示里的账号名对得上),凭证是还没落盘的新凭证。
|
|
19
|
+
*/
|
|
20
|
+
export function loginContext(ctx, credential) {
|
|
21
|
+
return { ...ctx, account: loginTarget(ctx), credential };
|
|
22
|
+
}
|
|
23
|
+
/** 登录方式对应的新凭证(还没落盘)。 */
|
|
24
|
+
export function freshCredential(ctx, method) {
|
|
25
|
+
return newCredential({ platform: ctx.platform.id, endpoint: ctx.endpoint, account: loginTarget(ctx), method });
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 登录成功后落盘:账号名下已经存了另一个用户时报 USAGE,新凭证不保存;
|
|
29
|
+
* 还没有当前账号时,把它设为当前账号。
|
|
30
|
+
*/
|
|
31
|
+
export async function finishLogin(ctx, credential, user) {
|
|
32
|
+
const p = ctx.platform.id;
|
|
33
|
+
const account = loginTarget(ctx);
|
|
34
|
+
const existing = await readCredential(p, ctx.endpoint, account).catch(() => null);
|
|
35
|
+
if (existing?.user && existing.user.id !== user.id) {
|
|
36
|
+
throw new SpecificAIError('USAGE', `账号名 ${account} 下已经保存了另一个用户(${existing.user.name ?? existing.user.id})`, {
|
|
37
|
+
hint: `用 -a <新名字> 登录,或者先 specific-ai ${p} auth logout -a ${account}${endpointFlag(ctx.endpoint)}`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
credential.account = account;
|
|
41
|
+
credential.user = user;
|
|
42
|
+
if (existing)
|
|
43
|
+
credential.created_at = existing.created_at;
|
|
44
|
+
await writeCredential(credential);
|
|
45
|
+
let current = await getCurrent(p, ctx.endpoint);
|
|
46
|
+
if (!current) {
|
|
47
|
+
await setCurrent(p, ctx.endpoint, account);
|
|
48
|
+
current = account;
|
|
49
|
+
}
|
|
50
|
+
ctx.log.info(`已登录 ${ctx.platform.name}:${user.name ?? user.id}(账号 ${account})`);
|
|
51
|
+
return toAccount(credential, current === account);
|
|
52
|
+
}
|
|
53
|
+
/** `--cookie <str|@file|->`:dispatch 已经把 @file 和 - 读成字符串。 */
|
|
54
|
+
export function cookieCredential(ctx, defaultDomain) {
|
|
55
|
+
const input = ctx.options.cookie;
|
|
56
|
+
if (!input?.trim()) {
|
|
57
|
+
throw new SpecificAIError('USAGE', 'cookie 登录需要 --cookie', { hint: `specific-ai ${ctx.platform.id} auth login --method cookie --cookie "<cookie>"` });
|
|
58
|
+
}
|
|
59
|
+
const credential = freshCredential(ctx, 'cookie');
|
|
60
|
+
try {
|
|
61
|
+
credential.scopes.main.cookies = parseCookieInput(input, defaultDomain);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new SpecificAIError('USAGE', '无法解析 --cookie 的内容', { hint: '传浏览器请求头里的 Cookie 字符串,或导出的 cookie JSON 数组' });
|
|
65
|
+
}
|
|
66
|
+
if (!credential.scopes.main.cookies.length)
|
|
67
|
+
throw new SpecificAIError('USAGE', '--cookie 里没有 cookie');
|
|
68
|
+
return credential;
|
|
69
|
+
}
|
|
70
|
+
/** 二维码:画到 stderr,同时把 PNG 存到 cache/<p>/qrcode.png。 */
|
|
71
|
+
export async function showQrcode(ctx, content, hint = '请用 App 扫码') {
|
|
72
|
+
const png = join(cacheDir(ctx.platform.id), 'qrcode.png');
|
|
73
|
+
await writeFileAtomic(png, await QRCode.toBuffer(content, { margin: 2, scale: 8 }));
|
|
74
|
+
const art = await QRCode.toString(content, { type: 'terminal', small: true });
|
|
75
|
+
process.stderr.write(art + '\n');
|
|
76
|
+
ctx.log.info(`${hint}。二维码图片:${png}`);
|
|
77
|
+
return png;
|
|
78
|
+
}
|
|
79
|
+
/** 轮询时最多容忍的连续网络错误次数(含最后一次)。 */
|
|
80
|
+
const POLL_NETWORK_RETRIES = 4;
|
|
81
|
+
/** 按间隔轮询,直到返回非 undefined 的值;超时报 AUTH_REQUIRED。 */
|
|
82
|
+
export async function poll(fn, options = {}) {
|
|
83
|
+
const deadline = now() + (options.timeout ?? 180_000);
|
|
84
|
+
// 扫码要等几分钟,期间偶发的网络错误(连接被重置、本机地址暂不可用等)不该让整个登录失败;连续失败才放弃
|
|
85
|
+
let networkErrors = 0;
|
|
86
|
+
for (;;) {
|
|
87
|
+
let result;
|
|
88
|
+
try {
|
|
89
|
+
result = await fn();
|
|
90
|
+
networkErrors = 0;
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (!(err instanceof SpecificAIError && err.code === 'NETWORK') || ++networkErrors >= POLL_NETWORK_RETRIES)
|
|
94
|
+
throw err;
|
|
95
|
+
process.stderr.write(`[specific-ai] 警告:轮询时网络出错,稍后重试(${networkErrors}/${POLL_NETWORK_RETRIES - 1}):${err.message}\n`);
|
|
96
|
+
}
|
|
97
|
+
if (result !== undefined)
|
|
98
|
+
return result;
|
|
99
|
+
if (now() >= deadline)
|
|
100
|
+
throw new SpecificAIError('AUTH_REQUIRED', `${options.what ?? '登录'}超时`, { hint: '重新执行登录命令' });
|
|
101
|
+
await sleep(options.interval ?? 2000);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** 是否可以交互输入(stdin 与 stderr 都是 TTY)。 */
|
|
105
|
+
export function interactive() {
|
|
106
|
+
return Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
107
|
+
}
|
|
108
|
+
/** 在 stderr 上提问,读一行。hidden 时不回显(密码)。 */
|
|
109
|
+
export async function prompt(question, options = {}) {
|
|
110
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
|
|
111
|
+
if (options.hidden) {
|
|
112
|
+
const write = rl._writeToOutput;
|
|
113
|
+
rl._writeToOutput = (s) => {
|
|
114
|
+
if (s.startsWith(question))
|
|
115
|
+
write.call(rl, s);
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
return await new Promise((resolve) => rl.question(question, resolve));
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
if (options.hidden)
|
|
123
|
+
process.stderr.write('\n');
|
|
124
|
+
rl.close();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** `--password-stdin`:TTY 下隐藏输入,否则读完整个 stdin。 */
|
|
128
|
+
export async function readPassword(ctx) {
|
|
129
|
+
if (!ctx.options.passwordStdin) {
|
|
130
|
+
throw new SpecificAIError('USAGE', '密码只从 stdin 读取,请加 --password-stdin', {
|
|
131
|
+
hint: `specific-ai ${ctx.platform.id} auth login --method password --username <name> --password-stdin`,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const password = process.stdin.isTTY ? await prompt('密码:', { hidden: true }) : (await readStreamText(process.stdin)).replace(/\r?\n$/, '');
|
|
135
|
+
if (!password)
|
|
136
|
+
throw new SpecificAIError('USAGE', '密码为空');
|
|
137
|
+
return password;
|
|
138
|
+
}
|
|
139
|
+
const SMS_TTL = 10 * 60 * 1000;
|
|
140
|
+
function smsFile(ctx) {
|
|
141
|
+
return join(cacheDir(ctx.platform.id), `sms-${ctx.endpoint}-${loginTarget(ctx)}.json`);
|
|
142
|
+
}
|
|
143
|
+
/** 短信登录的中间态(非 TTY 分两步时用),10 分钟有效。 */
|
|
144
|
+
export const smsState = {
|
|
145
|
+
async save(ctx, data) {
|
|
146
|
+
await writeFileAtomic(smsFile(ctx), JSON.stringify({ saved_at: now(), data }));
|
|
147
|
+
},
|
|
148
|
+
async load(ctx) {
|
|
149
|
+
try {
|
|
150
|
+
const { saved_at, data } = JSON.parse(await readFile(smsFile(ctx), 'utf8'));
|
|
151
|
+
return now() - saved_at < SMS_TTL ? data : null;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
async clear(ctx) {
|
|
158
|
+
await rm(smsFile(ctx), { force: true });
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* 短信登录的通用流程。平台提供 send(发验证码,返回需要保存的中间态)和 verify(用验证码完成登录)。
|
|
163
|
+
* TTY 下一步完成;非 TTY 下先 `--phone` 发码,再单独用 `--code` 完成。
|
|
164
|
+
*/
|
|
165
|
+
export async function smsLogin(ctx, flow) {
|
|
166
|
+
const phone = ctx.options.phone;
|
|
167
|
+
const code = ctx.options.code;
|
|
168
|
+
const p = ctx.platform.id;
|
|
169
|
+
const cmd = `specific-ai ${p} auth login --method sms${ctx.account ? ` -a ${ctx.account}` : ''}${endpointFlag(ctx.endpoint)}`;
|
|
170
|
+
if (code) {
|
|
171
|
+
const state = await smsState.load(ctx);
|
|
172
|
+
if (!state)
|
|
173
|
+
throw new SpecificAIError('USAGE', '没有找到有效的短信登录中间态(10 分钟内有效)', { hint: `${cmd} --phone <手机号>` });
|
|
174
|
+
const account = await flow.verify(state, code);
|
|
175
|
+
await smsState.clear(ctx);
|
|
176
|
+
return account;
|
|
177
|
+
}
|
|
178
|
+
if (!phone) {
|
|
179
|
+
if (!interactive())
|
|
180
|
+
throw new SpecificAIError('USAGE', '短信登录需要 --phone', { hint: `${cmd} --phone <手机号>` });
|
|
181
|
+
}
|
|
182
|
+
const target = phone ?? (await prompt('手机号:'));
|
|
183
|
+
const state = await flow.send(target);
|
|
184
|
+
if (!interactive()) {
|
|
185
|
+
await smsState.save(ctx, state);
|
|
186
|
+
ctx.log.info(`验证码已发送到 ${target}。收到后执行:${cmd} --code <验证码>`);
|
|
187
|
+
return { sent: true, phone: target };
|
|
188
|
+
}
|
|
189
|
+
const input = await prompt('验证码:');
|
|
190
|
+
return flow.verify(state, input.trim());
|
|
191
|
+
}
|
package/dist/core/mp4.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { SpecificAIError } from './errors.js';
|
|
2
|
+
/** 第一条视频轨(hdlr 为 vide)。不是 MP4 / MOV 时报 USAGE。 */
|
|
3
|
+
export function mp4VideoTrack(data) {
|
|
4
|
+
const buf = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
5
|
+
const boxes = (start, end) => {
|
|
6
|
+
const out = [];
|
|
7
|
+
let p = start;
|
|
8
|
+
while (p + 8 <= end) {
|
|
9
|
+
let size = buf.readUInt32BE(p);
|
|
10
|
+
const type = buf.toString('latin1', p + 4, p + 8);
|
|
11
|
+
let header = 8;
|
|
12
|
+
if (size === 1) {
|
|
13
|
+
if (p + 16 > end)
|
|
14
|
+
break;
|
|
15
|
+
size = Number(buf.readBigUInt64BE(p + 8));
|
|
16
|
+
header = 16;
|
|
17
|
+
}
|
|
18
|
+
else if (size === 0)
|
|
19
|
+
size = end - p;
|
|
20
|
+
if (size < header || p + size > end)
|
|
21
|
+
break;
|
|
22
|
+
out.push({ type, start: p + header, end: p + size });
|
|
23
|
+
p += size;
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
};
|
|
27
|
+
const find = (list, type) => list.find((b) => b.type === type);
|
|
28
|
+
const moov = find(boxes(0, buf.length), 'moov');
|
|
29
|
+
if (!moov)
|
|
30
|
+
throw new SpecificAIError('USAGE', '只支持 MP4 / MOV 视频(找不到 moov)');
|
|
31
|
+
for (const trak of boxes(moov.start, moov.end).filter((b) => b.type === 'trak')) {
|
|
32
|
+
const inner = boxes(trak.start, trak.end);
|
|
33
|
+
const mdia = find(inner, 'mdia');
|
|
34
|
+
const tkhd = find(inner, 'tkhd');
|
|
35
|
+
if (!mdia || !tkhd)
|
|
36
|
+
continue;
|
|
37
|
+
const mdiaBoxes = boxes(mdia.start, mdia.end);
|
|
38
|
+
const hdlr = find(mdiaBoxes, 'hdlr');
|
|
39
|
+
if (!hdlr || buf.toString('latin1', hdlr.start + 8, hdlr.start + 12) !== 'vide')
|
|
40
|
+
continue;
|
|
41
|
+
// tkhd 末尾:矩阵 9 × int32,然后宽、高(16.16)
|
|
42
|
+
const displayWidth = Math.round(buf.readUInt32BE(tkhd.end - 8) / 65536);
|
|
43
|
+
const displayHeight = Math.round(buf.readUInt32BE(tkhd.end - 4) / 65536);
|
|
44
|
+
const matrix = tkhd.end - 8 - 36;
|
|
45
|
+
const a = buf.readInt32BE(matrix) / 65536;
|
|
46
|
+
const b = buf.readInt32BE(matrix + 4) / 65536;
|
|
47
|
+
const rotation = (((Math.round((Math.atan2(b, a) * 180) / Math.PI) % 360) + 360) % 360) || 0;
|
|
48
|
+
const mdhd = find(mdiaBoxes, 'mdhd');
|
|
49
|
+
let timescale = 0;
|
|
50
|
+
let duration = 0;
|
|
51
|
+
if (mdhd) {
|
|
52
|
+
const v1 = buf[mdhd.start] === 1;
|
|
53
|
+
timescale = buf.readUInt32BE(mdhd.start + (v1 ? 20 : 12));
|
|
54
|
+
duration = v1 ? Number(buf.readBigUInt64BE(mdhd.start + 24)) : buf.readUInt32BE(mdhd.start + 16);
|
|
55
|
+
}
|
|
56
|
+
const minf = find(mdiaBoxes, 'minf');
|
|
57
|
+
const stbl = minf && find(boxes(minf.start, minf.end), 'stbl');
|
|
58
|
+
const stblBoxes = stbl ? boxes(stbl.start, stbl.end) : [];
|
|
59
|
+
let sttsSamples = 0;
|
|
60
|
+
let sttsDuration = 0;
|
|
61
|
+
const stts = find(stblBoxes, 'stts');
|
|
62
|
+
if (stts) {
|
|
63
|
+
const n = buf.readUInt32BE(stts.start + 4);
|
|
64
|
+
for (let i = 0; i < n && stts.start + 16 + i * 8 <= stts.end; i++) {
|
|
65
|
+
const count = buf.readUInt32BE(stts.start + 8 + i * 8);
|
|
66
|
+
sttsSamples += count;
|
|
67
|
+
sttsDuration += count * buf.readUInt32BE(stts.start + 12 + i * 8);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const stsz = find(stblBoxes, 'stsz');
|
|
71
|
+
const sampleCount = stsz ? buf.readUInt32BE(stsz.start + 8) : sttsSamples;
|
|
72
|
+
// stsd:版本 / 标志 4 + 条目数 4,第一个样本项的头 8 + 保留 6 + 引用索引 2 + 预定义 / 保留 16,然后宽、高(uint16)
|
|
73
|
+
let codedWidth = 0;
|
|
74
|
+
let codedHeight = 0;
|
|
75
|
+
const stsd = find(stblBoxes, 'stsd');
|
|
76
|
+
if (stsd && stsd.start + 8 + 36 <= stsd.end) {
|
|
77
|
+
codedWidth = buf.readUInt16BE(stsd.start + 8 + 32);
|
|
78
|
+
codedHeight = buf.readUInt16BE(stsd.start + 8 + 34);
|
|
79
|
+
}
|
|
80
|
+
return { displayWidth, displayHeight, codedWidth, codedHeight, rotation, timescale, duration, sttsSamples, sttsDuration, sampleCount };
|
|
81
|
+
}
|
|
82
|
+
throw new SpecificAIError('USAGE', '视频里没有可识别的视频轨(尺寸 / 时长)');
|
|
83
|
+
}
|
|
84
|
+
/** libavutil 的 av_reduce:约分,超过 max 时用连分数逼近。 */
|
|
85
|
+
export function avReduce(num, den, max) {
|
|
86
|
+
const abs = (x) => (x < 0n ? -x : x);
|
|
87
|
+
const gcd = (x, y) => (y === 0n ? x : gcd(y, x % y));
|
|
88
|
+
const sign = num < 0n !== den < 0n;
|
|
89
|
+
const g = gcd(abs(num), abs(den));
|
|
90
|
+
if (g) {
|
|
91
|
+
num = abs(num) / g;
|
|
92
|
+
den = abs(den) / g;
|
|
93
|
+
}
|
|
94
|
+
let a0 = [0n, 1n];
|
|
95
|
+
let a1 = [1n, 0n];
|
|
96
|
+
if (num <= max && den <= max) {
|
|
97
|
+
a1 = [num, den];
|
|
98
|
+
den = 0n;
|
|
99
|
+
}
|
|
100
|
+
while (den) {
|
|
101
|
+
let x = num / den;
|
|
102
|
+
const next = num - den * x;
|
|
103
|
+
const a2n = x * a1[0] + a0[0];
|
|
104
|
+
const a2d = x * a1[1] + a0[1];
|
|
105
|
+
if (a2n > max || a2d > max) {
|
|
106
|
+
if (a1[0])
|
|
107
|
+
x = (max - a0[0]) / a1[0];
|
|
108
|
+
if (a1[1]) {
|
|
109
|
+
const y = (max - a0[1]) / a1[1];
|
|
110
|
+
if (y < x)
|
|
111
|
+
x = y;
|
|
112
|
+
}
|
|
113
|
+
if (den * (2n * x * a1[1] + a0[1]) > num * a1[1])
|
|
114
|
+
a1 = [x * a1[0] + a0[0], x * a1[1] + a0[1]];
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
a0 = a1;
|
|
118
|
+
a1 = [a2n, a2d];
|
|
119
|
+
num = den;
|
|
120
|
+
den = next;
|
|
121
|
+
}
|
|
122
|
+
return [sign ? -a1[0] : a1[0], a1[1]];
|
|
123
|
+
}
|
|
124
|
+
/** ffmpeg mov 解复用器的 avg_frame_rate:time_scale × 帧数 / 帧时长之和(opencv 的 CAP_PROP_FPS 取它)。 */
|
|
125
|
+
export function mp4AvgFrameRate(t) {
|
|
126
|
+
if (!(t.sttsDuration > 0 && t.sttsSamples > 0 && t.timescale > 0))
|
|
127
|
+
return 0;
|
|
128
|
+
const [num, den] = avReduce(BigInt(t.timescale) * BigInt(t.sttsSamples), BigInt(t.sttsDuration), 0x7fffffffn);
|
|
129
|
+
return Number(num) / Number(den);
|
|
130
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { isoNow } from './fsutil.js';
|
|
2
|
+
import { now } from './rand.js';
|
|
3
|
+
import { withRaw } from './schemas.js';
|
|
4
|
+
/**
|
|
5
|
+
* 归一化工具(AGENTS 6.2):计数、时间、ID 的转换,以及各类型的构造函数。
|
|
6
|
+
* 构造函数补齐缺省字段(null / []),保证"字段总是存在";传入 raw 时挂上原始对象供 `--raw` 使用。
|
|
7
|
+
*/
|
|
8
|
+
const UNITS = { 万: 1e4, w: 1e4, W: 1e4, 亿: 1e8, k: 1e3, K: 1e3, m: 1e6, M: 1e6, 千: 1e3 };
|
|
9
|
+
/** 计数转整数:`1.2万` → 12000,`10w+` → 100000,`1,234` → 1234。无法识别时为 null。 */
|
|
10
|
+
export function count(value) {
|
|
11
|
+
if (value == null || value === '')
|
|
12
|
+
return null;
|
|
13
|
+
if (typeof value === 'number')
|
|
14
|
+
return Number.isFinite(value) ? Math.round(value) : null;
|
|
15
|
+
if (typeof value === 'bigint')
|
|
16
|
+
return Number(value);
|
|
17
|
+
const m = /^\s*([\d.,]+)\s*([万wW亿kKmM千]?)\+?\s*$/.exec(String(value));
|
|
18
|
+
if (!m)
|
|
19
|
+
return null;
|
|
20
|
+
const n = Number(m[1].replaceAll(',', ''));
|
|
21
|
+
return Number.isFinite(n) ? Math.round(n * (m[2] ? UNITS[m[2]] : 1)) : null;
|
|
22
|
+
}
|
|
23
|
+
/** ID 一律是字符串。 */
|
|
24
|
+
export function id(value) {
|
|
25
|
+
return value == null ? '' : String(value);
|
|
26
|
+
}
|
|
27
|
+
export function idOrNull(value) {
|
|
28
|
+
return value == null || value === '' ? null : String(value);
|
|
29
|
+
}
|
|
30
|
+
export function str(value) {
|
|
31
|
+
return value == null || value === '' ? null : String(value);
|
|
32
|
+
}
|
|
33
|
+
/** 不带时区的日期时间:`2025-08-01 12:00`、`2025-08-01T12:00:00`。 */
|
|
34
|
+
const NAIVE_TIME = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2})(:\d{2}(?:\.\d+)?)?$/;
|
|
35
|
+
/**
|
|
36
|
+
* 时间转带时区的 ISO 8601。数字按量级判断:小于 1e11 视为秒,小于 1e14 为毫秒,再大是微秒;
|
|
37
|
+
* 字符串先按数字处理;不带时区的日期时间按北京时间(这些平台给的本地时间都是北京时间,
|
|
38
|
+
* 不能按运行 specific-ai 的机器所在时区解析);其余交给 Date 解析。
|
|
39
|
+
*/
|
|
40
|
+
export function time(value) {
|
|
41
|
+
if (value == null || value === '' || value === 0 || value === '0')
|
|
42
|
+
return null;
|
|
43
|
+
let ms;
|
|
44
|
+
if (typeof value === 'number' || (typeof value === 'string' && /^\d+(\.\d+)?$/.test(value))) {
|
|
45
|
+
const n = Number(value);
|
|
46
|
+
ms = n < 1e11 ? n * 1000 : n < 1e14 ? n : Math.floor(n / 1000);
|
|
47
|
+
}
|
|
48
|
+
else if (value instanceof Date)
|
|
49
|
+
ms = value.getTime();
|
|
50
|
+
else {
|
|
51
|
+
const s = String(value).trim();
|
|
52
|
+
const naive = NAIVE_TIME.exec(s);
|
|
53
|
+
ms = Date.parse(naive ? `${naive[1]}T${naive[2]}${naive[3] ?? ':00'}+08:00` : s);
|
|
54
|
+
}
|
|
55
|
+
return Number.isFinite(ms) ? isoNow(new Date(ms)) : null;
|
|
56
|
+
}
|
|
57
|
+
/** 秒数(可带小数);毫秒请先除以 1000。 */
|
|
58
|
+
export function seconds(value) {
|
|
59
|
+
if (value == null || value === '')
|
|
60
|
+
return null;
|
|
61
|
+
const n = Number(value);
|
|
62
|
+
return Number.isFinite(n) ? n : null;
|
|
63
|
+
}
|
|
64
|
+
/** `//host/path` 与 `http://` 统一成 `https://`。 */
|
|
65
|
+
export function url(value) {
|
|
66
|
+
if (value == null || value === '')
|
|
67
|
+
return null;
|
|
68
|
+
const s = String(value);
|
|
69
|
+
if (s.startsWith('//'))
|
|
70
|
+
return 'https:' + s;
|
|
71
|
+
if (s.startsWith('http://'))
|
|
72
|
+
return 'https://' + s.slice(7);
|
|
73
|
+
return s;
|
|
74
|
+
}
|
|
75
|
+
/** 金额(元):数字,或 `¥1,099.00`、`总额 -23.28` 这类带符号、千分位的字符串。 */
|
|
76
|
+
export function price(amount, currency = 'CNY') {
|
|
77
|
+
if (amount == null || amount === '')
|
|
78
|
+
return null;
|
|
79
|
+
const m = typeof amount === 'number' ? null : /-?\d[\d,]*(?:\.\d+)?/.exec(String(amount));
|
|
80
|
+
const n = typeof amount === 'number' ? amount : m ? Number(m[0].replaceAll(',', '')) : NaN;
|
|
81
|
+
return Number.isFinite(n) ? { amount: n, currency } : null;
|
|
82
|
+
}
|
|
83
|
+
const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ', yen: '¥', middot: '·', hellip: '…', ldquo: '“', rdquo: '”', lsquo: '‘', rsquo: '’', mdash: '—', ndash: '–', times: '×' };
|
|
84
|
+
/** HTML 实体解码(`html.unescape` 的常用子集):命名实体与 `'` / `'` 这类数字实体。 */
|
|
85
|
+
export function unescapeHtml(s) {
|
|
86
|
+
return s.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);?/gi, (m, e) => {
|
|
87
|
+
if (e[0] === '#') {
|
|
88
|
+
const code = e[1] === 'x' || e[1] === 'X' ? parseInt(e.slice(2), 16) : parseInt(e.slice(1), 10);
|
|
89
|
+
return Number.isFinite(code) && code <= 0x10ffff ? String.fromCodePoint(code) : m;
|
|
90
|
+
}
|
|
91
|
+
return ENTITIES[e.toLowerCase()] ?? m;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* HTML 片段 → 纯文本:`<br>` 还原成换行,图片(表情)取 alt,其余标签去掉,实体解码。空串为 null。
|
|
96
|
+
* 用于搜索结果的高亮标签、微博正文等。
|
|
97
|
+
*/
|
|
98
|
+
export function plainText(html) {
|
|
99
|
+
if (html == null || html === '')
|
|
100
|
+
return null;
|
|
101
|
+
const text = unescapeHtml(String(html)
|
|
102
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
103
|
+
.replace(/<img[^>]*?\balt=(["'])(.*?)\1[^>]*>/gi, '$2')
|
|
104
|
+
.replace(/<[^>]+>/g, ''));
|
|
105
|
+
return text === '' ? null : text;
|
|
106
|
+
}
|
|
107
|
+
function attach(value, raw) {
|
|
108
|
+
return raw === undefined ? value : withRaw(value, raw);
|
|
109
|
+
}
|
|
110
|
+
export function userRef(v) {
|
|
111
|
+
if (!v || v.id == null || v.id === '')
|
|
112
|
+
return null;
|
|
113
|
+
return { id: id(v.id), name: str(v.name), url: url(v.url) };
|
|
114
|
+
}
|
|
115
|
+
export function user(v, raw) {
|
|
116
|
+
return attach({
|
|
117
|
+
id: v.id,
|
|
118
|
+
name: v.name ?? null,
|
|
119
|
+
handle: v.handle ?? null,
|
|
120
|
+
avatar: v.avatar ?? null,
|
|
121
|
+
url: v.url ?? null,
|
|
122
|
+
bio: v.bio ?? null,
|
|
123
|
+
stats: { followers: null, following: null, items: null, likes: null, ...v.stats },
|
|
124
|
+
}, raw);
|
|
125
|
+
}
|
|
126
|
+
export function media(v, raw) {
|
|
127
|
+
return attach({ id: v.id ?? null, type: v.type, url: v.url, width: v.width ?? null, height: v.height ?? null, duration: v.duration ?? null }, raw);
|
|
128
|
+
}
|
|
129
|
+
export function item(v, raw) {
|
|
130
|
+
return attach({
|
|
131
|
+
id: v.id,
|
|
132
|
+
kind: v.kind,
|
|
133
|
+
url: v.url ?? null,
|
|
134
|
+
title: v.title ?? null,
|
|
135
|
+
text: v.text ?? null,
|
|
136
|
+
author: v.author ?? null,
|
|
137
|
+
created_at: v.created_at ?? null,
|
|
138
|
+
cover: v.cover ?? null,
|
|
139
|
+
media: v.media ?? [],
|
|
140
|
+
stats: { views: null, likes: null, comments: null, collects: null, shares: null, ...v.stats },
|
|
141
|
+
price: v.price ?? null,
|
|
142
|
+
status: v.status ?? null,
|
|
143
|
+
}, raw);
|
|
144
|
+
}
|
|
145
|
+
export function comment(v, raw) {
|
|
146
|
+
return attach({
|
|
147
|
+
id: v.id,
|
|
148
|
+
item_id: v.item_id,
|
|
149
|
+
parent_id: v.parent_id ?? null,
|
|
150
|
+
author: v.author ?? null,
|
|
151
|
+
text: v.text,
|
|
152
|
+
created_at: v.created_at ?? null,
|
|
153
|
+
stats: { likes: null, replies: null, ...v.stats },
|
|
154
|
+
}, raw);
|
|
155
|
+
}
|
|
156
|
+
export function folder(v, raw) {
|
|
157
|
+
return attach({ id: v.id, name: v.name, count: v.count ?? null, url: v.url ?? null }, raw);
|
|
158
|
+
}
|
|
159
|
+
export function category(v, raw) {
|
|
160
|
+
return attach({ id: v.id, name: v.name, parent_id: v.parent_id ?? null }, raw);
|
|
161
|
+
}
|
|
162
|
+
export function keyword(v, raw) {
|
|
163
|
+
return attach({ text: v.text, heat: v.heat ?? null }, raw);
|
|
164
|
+
}
|
|
165
|
+
export function topic(v, raw) {
|
|
166
|
+
return attach({ id: v.id, name: v.name, url: v.url ?? null, stats: { views: null, items: null, ...v.stats } }, raw);
|
|
167
|
+
}
|
|
168
|
+
export function poi(v, raw) {
|
|
169
|
+
return attach({ id: v.id, name: v.name, address: v.address ?? null }, raw);
|
|
170
|
+
}
|
|
171
|
+
export function live(v, raw) {
|
|
172
|
+
return attach({ id: v.id, url: v.url ?? null, title: v.title ?? null, status: v.status, host: v.host ?? null, cover: v.cover ?? null, stats: { viewers: null, ...v.stats } }, raw);
|
|
173
|
+
}
|
|
174
|
+
export function event(v, raw) {
|
|
175
|
+
return attach({ type: v.type, time: v.time ?? isoNow(new Date(now())), user: v.user ?? null, text: v.text ?? null, gift: v.gift ?? null }, raw);
|
|
176
|
+
}
|
|
177
|
+
export function gift(v, raw) {
|
|
178
|
+
return attach({ id: v.id, name: v.name, price: v.price ?? null }, raw);
|
|
179
|
+
}
|
|
180
|
+
export function rank(v, raw) {
|
|
181
|
+
return attach({ rank: v.rank, user: v.user, score: v.score ?? null }, raw);
|
|
182
|
+
}
|
|
183
|
+
export function conversation(v, raw) {
|
|
184
|
+
return attach({ id: v.id, peer: v.peer ?? null, unread: v.unread ?? null, last_message: v.last_message ?? null, updated_at: v.updated_at ?? null }, raw);
|
|
185
|
+
}
|
|
186
|
+
export function message(v, raw) {
|
|
187
|
+
return attach({
|
|
188
|
+
id: v.id,
|
|
189
|
+
conversation_id: v.conversation_id,
|
|
190
|
+
from: v.from ?? null,
|
|
191
|
+
type: v.type ?? 'text',
|
|
192
|
+
text: v.text ?? null,
|
|
193
|
+
media: v.media ?? [],
|
|
194
|
+
created_at: v.created_at ?? null,
|
|
195
|
+
}, raw);
|
|
196
|
+
}
|
|
197
|
+
export function notice(v, raw) {
|
|
198
|
+
return attach({ id: v.id, type: v.type, user: v.user ?? null, target: v.target ?? null, text: v.text ?? null, created_at: v.created_at ?? null }, raw);
|
|
199
|
+
}
|
|
200
|
+
export function noticeCount(v, raw) {
|
|
201
|
+
const parts = { comment: v.comment ?? null, mention: v.mention ?? null, like: v.like ?? null, follow: v.follow ?? null, system: v.system ?? null };
|
|
202
|
+
const total = v.total ?? Object.values(parts).reduce((s, n) => s + (n ?? 0), 0);
|
|
203
|
+
return attach({ total, ...parts }, raw);
|
|
204
|
+
}
|
|
205
|
+
export function file(v) {
|
|
206
|
+
return v;
|
|
207
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** 标准选项(AGENTS 4.9)。平台私有选项不能与它们重名。 */
|
|
3
|
+
export const PAGING = {
|
|
4
|
+
limit: z.number().int().positive().optional().describe('取满 N 条,自动翻页'),
|
|
5
|
+
cursor: z.string().optional().describe('从上次返回的 page.cursor 继续翻'),
|
|
6
|
+
all: z.boolean().optional().describe('一直翻到没有更多'),
|
|
7
|
+
};
|
|
8
|
+
export const FILTER_VALUES = {
|
|
9
|
+
sort: ['general', 'latest', 'popular', 'views', 'comments', 'collects', 'sales', 'price_asc', 'price_desc'],
|
|
10
|
+
type: ['all', 'video', 'image', 'text', 'article', 'goods'],
|
|
11
|
+
time: ['all', 'day', 'week', 'month', 'half_year', 'year'],
|
|
12
|
+
kind: ['recommend', 'hot', 'following'],
|
|
13
|
+
};
|
|
14
|
+
export const LOGIN_METHODS = ['qrcode', 'sms', 'password', 'cookie'];
|
|
15
|
+
export const VISIBILITY_VALUES = ['public', 'private', 'friends', 'fans'];
|
|
16
|
+
/**
|
|
17
|
+
* 有标准取值的选项。取值是标准值、但平台不支持时报 UNSUPPORTED;不是标准值时报 USAGE。
|
|
18
|
+
*/
|
|
19
|
+
export const STANDARD_VALUES = { ...FILTER_VALUES, method: LOGIN_METHODS, visibility: VISIBILITY_VALUES };
|
|
20
|
+
const FILTER_SUMMARY = { sort: '排序', type: '类型', time: '时间范围', kind: '流的种类' };
|
|
21
|
+
function makeFilter(name, values, preferred) {
|
|
22
|
+
if (values.length === 0)
|
|
23
|
+
throw new Error(`filter.${name}() 至少要一个取值`);
|
|
24
|
+
const schema = z.enum(values).describe(FILTER_SUMMARY[name]);
|
|
25
|
+
return preferred && values.includes(preferred) ? schema.default(preferred) : schema.optional();
|
|
26
|
+
}
|
|
27
|
+
/** 在注册表里声明平台支持的筛选取值,例如 `filter.sort('general', 'latest')`。 */
|
|
28
|
+
export const filter = {
|
|
29
|
+
sort: (...values) => makeFilter('sort', values),
|
|
30
|
+
type: (...values) => makeFilter('type', values),
|
|
31
|
+
time: (...values) => makeFilter('time', values),
|
|
32
|
+
kind: (...values) => makeFilter('kind', values, 'recommend'),
|
|
33
|
+
};
|
|
34
|
+
/** `--visibility` 的取值。平台支持的与默认的 public / private / friends 不同时,在注册表里声明,例如 `visibility('public', 'fans')`。 */
|
|
35
|
+
export const visibility = (...values) => z.enum(values).default('public').describe('可见范围');
|
|
36
|
+
export const CATEGORY = z.string().optional().describe('分类 id,取值来自对应的 categories 命令');
|
|
37
|
+
/** `comment list --product`:传纯 ID 时按商品处理,列出商品评价(AGENTS 4.8)。 */
|
|
38
|
+
export const PRODUCT = z.boolean().optional().describe('把参数当作商品,列出商品评价');
|
|
39
|
+
export const PUBLISH = {
|
|
40
|
+
title: z.string().optional().describe('标题'),
|
|
41
|
+
text: z.string().optional().describe('正文,@file 表示从文件读取'),
|
|
42
|
+
image: z.array(z.string()).optional().describe('图片(路径或 URL)'),
|
|
43
|
+
video: z.string().optional().describe('视频(路径或 URL)'),
|
|
44
|
+
cover: z.string().optional().describe('封面(路径或 URL)'),
|
|
45
|
+
tag: z.array(z.string()).optional().describe('标签'),
|
|
46
|
+
topic: z.array(z.string()).optional().describe('话题'),
|
|
47
|
+
mention: z.array(z.string()).optional().describe('@ 的用户'),
|
|
48
|
+
poi: z.string().optional().describe('地点 id'),
|
|
49
|
+
category: CATEGORY,
|
|
50
|
+
visibility: visibility('public', 'private', 'friends'),
|
|
51
|
+
schedule: z.iso.datetime({ offset: true, local: true }).optional().describe('定时发布(ISO 时间)'),
|
|
52
|
+
price: z.number().positive().optional().describe('商品价格(元)'),
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* `--quote <item>`:发成引用(AGENTS 4.9「发布」)。不放进 PUBLISH:目前只有 x 支持,由 x 在注册表里声明,
|
|
56
|
+
* 其他平台的 item publish 不接受这个选项。
|
|
57
|
+
*/
|
|
58
|
+
export const QUOTE = z.string().optional().describe('引用一条内容:ID 或 URL');
|
|
59
|
+
export const DOWNLOAD = {
|
|
60
|
+
dir: z.string().optional().describe('下载目录,默认当前目录'),
|
|
61
|
+
overwrite: z.boolean().optional().describe('覆盖已有文件,默认跳过'),
|
|
62
|
+
};
|
|
63
|
+
const DURATION_RE = /^(\d+(?:\.\d+)?)(s|m|h)?$/;
|
|
64
|
+
export const STREAM = {
|
|
65
|
+
duration: z.string().regex(DURATION_RE, '格式为秒数,或 30s、10m、1h').optional().describe('运行时长,例如 600、10m、1h'),
|
|
66
|
+
};
|
|
67
|
+
/** `--duration` 转成毫秒。 */
|
|
68
|
+
export function parseDuration(value) {
|
|
69
|
+
const m = DURATION_RE.exec(value);
|
|
70
|
+
if (!m)
|
|
71
|
+
throw new Error(`无效的时长:${value}`);
|
|
72
|
+
const unit = { s: 1, m: 60, h: 3600 }[(m[2] ?? 's')];
|
|
73
|
+
return Number(m[1]) * unit * 1000;
|
|
74
|
+
}
|
|
75
|
+
export const STANDARD_OPTIONS = {
|
|
76
|
+
...PAGING,
|
|
77
|
+
...Object.fromEntries(Object.keys(FILTER_VALUES).map((k) => [k, z.string().optional()])),
|
|
78
|
+
...PUBLISH,
|
|
79
|
+
quote: QUOTE,
|
|
80
|
+
...DOWNLOAD,
|
|
81
|
+
...STREAM,
|
|
82
|
+
};
|
|
83
|
+
/** 从 zod schema 读出 argv 需要的信息:类型、是否必填、默认值、枚举值、说明。 */
|
|
84
|
+
export function describeOption(schema) {
|
|
85
|
+
let s = schema;
|
|
86
|
+
let required = true;
|
|
87
|
+
let dflt;
|
|
88
|
+
let description = s.description ?? null;
|
|
89
|
+
for (;;) {
|
|
90
|
+
const def = s._zod.def;
|
|
91
|
+
if (def.type === 'optional' || def.type === 'nullable')
|
|
92
|
+
required = false;
|
|
93
|
+
else if (def.type === 'default') {
|
|
94
|
+
required = false;
|
|
95
|
+
dflt = def.defaultValue;
|
|
96
|
+
}
|
|
97
|
+
else
|
|
98
|
+
break;
|
|
99
|
+
s = def.innerType;
|
|
100
|
+
description ??= s.description ?? null;
|
|
101
|
+
}
|
|
102
|
+
const type = s._zod.def.type;
|
|
103
|
+
const kind = type === 'boolean' ? 'boolean' : type === 'number' ? 'number' : type === 'array' ? 'array' : 'string';
|
|
104
|
+
const values = type === 'enum' ? s.options : null;
|
|
105
|
+
return { kind, required, default: dflt, values, description };
|
|
106
|
+
}
|
|
107
|
+
/** camelCase 键名 → kebab-case 选项名。 */
|
|
108
|
+
export function flagName(key) {
|
|
109
|
+
return key.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase());
|
|
110
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
/** 包根目录:从本文件向上找 name 为 specific-ai-cli 的 package.json(src/、dist/ 或其他编译输出目录都适用)。 */
|
|
6
|
+
export const PACKAGE_ROOT = findRoot(dirname(fileURLToPath(import.meta.url)));
|
|
7
|
+
function findRoot(dir) {
|
|
8
|
+
for (let d = dir;; d = dirname(d)) {
|
|
9
|
+
try {
|
|
10
|
+
if (JSON.parse(readFileSync(join(d, 'package.json'), 'utf8')).name === 'specific-ai-cli')
|
|
11
|
+
return d;
|
|
12
|
+
}
|
|
13
|
+
catch { }
|
|
14
|
+
if (dirname(d) === d)
|
|
15
|
+
return resolve(dir, '../..');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function packageJson() {
|
|
19
|
+
return JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
/** 数据目录:SPECIFIC_AI_HOME 或 ~/.specific-ai。 */
|
|
22
|
+
export function specificAiHome() {
|
|
23
|
+
const env = process.env.SPECIFIC_AI_HOME;
|
|
24
|
+
return env ? resolve(env) : join(homedir(), '.specific-ai');
|
|
25
|
+
}
|
|
26
|
+
export function configFile() {
|
|
27
|
+
return join(specificAiHome(), 'config.toml');
|
|
28
|
+
}
|
|
29
|
+
export function authDir(...parts) {
|
|
30
|
+
return join(specificAiHome(), 'auth', ...parts);
|
|
31
|
+
}
|
|
32
|
+
export function cacheDir(platform) {
|
|
33
|
+
return join(specificAiHome(), 'cache', platform);
|
|
34
|
+
}
|
|
35
|
+
/** 上游签名 JS 等静态资源:static/<p>/<file>。 */
|
|
36
|
+
export function staticFile(platform, file) {
|
|
37
|
+
return join(PACKAGE_ROOT, 'static', platform, file);
|
|
38
|
+
}
|